From 8b8a625fce35cc3309149a6adb32c9d850fddb68 Mon Sep 17 00:00:00 2001 From: Arnob kumar saha Date: Sun, 4 Jul 2021 18:52:56 +0600 Subject: [PATCH] fabric integrated --- .github/settings.yml | 14 + .gitignore | 21 + CHANGELOG.md | 2660 +++++++++++++++ CODEOWNERS | 4 + CODE_OF_CONDUCT.md | 8 + CONTRIBUTING.md | 18 + LICENSE | 202 ++ MAINTAINERS.md | 20 + README.md | 59 + SECURITY.md | 12 + asset-transfer-abac/README.md | 173 + asset-transfer-abac/chaincode-go/go.mod | 5 + asset-transfer-abac/chaincode-go/go.sum | 138 + .../chaincode-go/smart-contract/abac.go | 214 ++ .../chaincode-go/smartContract.go | 23 + asset-transfer-basic/.gitignore | 2 + .../application-go/.gitignore | 4 + .../application-go/assetTransfer.go | 155 + asset-transfer-basic/application-go/go.mod | 5 + asset-transfer-basic/application-go/go.sum | 277 ++ .../application-java/.gitattributes | 6 + .../application-java/build.gradle | 43 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + asset-transfer-basic/application-java/gradlew | 185 ++ .../application-java/gradlew.bat | 104 + .../application-java/settings.gradle | 10 + .../src/main/java/application/java/App.java | 116 + .../java/application/java/EnrollAdmin.java | 53 + .../java/application/java/RegisterUser.java | 107 + .../src/main/resources/log4j.properties | 19 + .../application-javascript/.eslintignore | 5 + .../application-javascript/.eslintrc.js | 36 + .../application-javascript/.gitignore | 14 + .../application-javascript/app.js | 180 ++ .../application-javascript/package.json | 16 + .../application-typescript/.gitignore | 15 + .../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 + .../chaincode-external/.dockerignore | 5 + .../chaincode-external/.gitignore | 2 + .../chaincode-external/Dockerfile | 17 + .../chaincode-external/README.md | 161 + .../chaincode-external/assetTransfer.go | 235 ++ .../chaincode-external/chaincode.env | 8 + .../chaincode-external/connection.json | 5 + .../chaincode-external/go.mod | 8 + .../chaincode-external/go.sum | 146 + .../chaincode-external/metadata.json | 4 + .../sampleBuilder/bin/detect | 14 + .../sampleBuilder/bin/release | 22 + .../chaincode-go/assetTransfer.go | 23 + .../chaincode/mocks/chaincodestub.go | 2878 +++++++++++++++++ .../chaincode/mocks/statequeryiterator.go | 232 ++ .../chaincode/mocks/transaction.go | 164 + .../chaincode-go/chaincode/smartcontract.go | 185 ++ .../chaincode/smartcontract_test.go | 184 ++ asset-transfer-basic/chaincode-go/go.mod | 11 + asset-transfer-basic/chaincode-go/go.sum | 146 + .../chaincode-java/.gitattributes | 6 + .../chaincode-java/build.gradle | 75 + .../config/checkstyle/checkstyle.xml | 171 + .../config/checkstyle/suppressions.xml | 9 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + asset-transfer-basic/chaincode-java/gradlew | 188 ++ .../chaincode-java/gradlew.bat | 100 + .../chaincode-java/settings.gradle | 5 + .../fabric/samples/assettransfer/Asset.java | 93 + .../samples/assettransfer/AssetTransfer.java | 236 ++ .../samples/assettransfer/AssetTest.java | 74 + .../assettransfer/AssetTransferTest.java | 305 ++ .../chaincode-javascript/.eslintignore | 5 + .../chaincode-javascript/.eslintrc.js | 39 + .../chaincode-javascript/.gitignore | 15 + .../chaincode-javascript/index.js | 12 + .../chaincode-javascript/lib/assetTransfer.js | 153 + .../chaincode-javascript/package.json | 49 + .../test/assetTransfer.test.js | 262 ++ .../chaincode-typescript/.gitignore | 16 + .../chaincode-typescript/package.json | 62 + .../chaincode-typescript/src/asset.ts | 26 + .../chaincode-typescript/src/assetTransfer.ts | 157 + .../chaincode-typescript/src/index.ts | 9 + .../chaincode-typescript/tsconfig.json | 18 + .../chaincode-typescript/tslint.json | 23 + asset-transfer-events/README.md | 91 + .../application-javascript/.eslintignore | 5 + .../application-javascript/.eslintrc.js | 36 + .../application-javascript/.gitignore | 14 + .../application-javascript/app.js | 545 ++++ .../application-javascript/package.json | 16 + .../chaincode-java/build.gradle | 51 + .../config/checkstyle/checkstyle.xml | 171 + .../config/checkstyle/suppressions.xml | 8 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58695 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + asset-transfer-events/chaincode-java/gradlew | 183 ++ .../chaincode-java/gradlew.bat | 100 + .../chaincode-java/settings.gradle | 5 + .../fabric/samples/events/Asset.java | 158 + .../fabric/samples/events/AssetTransfer.java | 299 ++ .../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 ++ .../application-java/.gitattributes | 6 + .../application-java/build.gradle | 43 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../application-java/gradlew | 185 ++ .../application-java/gradlew.bat | 104 + .../application-java/settings.gradle | 10 + .../src/main/java/application/java/App.java | 142 + .../java/application/java/EnrollAdmin.java | 53 + .../java/application/java/RegisterUser.java | 107 + .../src/main/resources/log4j.properties | 19 + .../application-javascript/.eslintignore | 5 + .../application-javascript/.eslintrc.js | 36 + .../application-javascript/.gitignore | 14 + .../application-javascript/app.js | 244 ++ .../application-javascript/package.json | 16 + .../statedb/couchdb/indexes/indexOwner.json | 1 + .../asset_transfer_ledger_chaincode.go | 466 +++ .../chaincode-go/go.mod | 9 + .../chaincode-go/go.sum | 146 + .../chaincode-javascript/.eslintignore | 5 + .../chaincode-javascript/.eslintrc.js | 37 + .../chaincode-javascript/index.js | 13 + .../statedb/couchdb/indexes/indexOwner.json | 1 + .../lib/asset_transfer_ledger_chaincode.js | 402 +++ .../chaincode-javascript/package.json | 19 + .../application-javascript/.eslintignore | 5 + .../application-javascript/.eslintrc.js | 37 + .../application-javascript/.gitignore | 16 + .../application-javascript/app.js | 358 ++ .../application-javascript/package.json | 45 + .../assetCollection/indexes/indexOwner.json | 12 + .../chaincode-go/README.md | 1 + .../chaincode-go/chaincode/asset_queries.go | 190 ++ .../chaincode/asset_queries_test.go | 185 ++ .../chaincode-go/chaincode/asset_transfer.go | 590 ++++ .../chaincode/asset_transfer_test.go | 444 +++ .../chaincode/mocks/chaincodestub.go | 2878 +++++++++++++++++ .../chaincode/mocks/clientIdentity.go | 399 +++ .../chaincode/mocks/statequeryiterator.go | 232 ++ .../chaincode/mocks/transaction.go | 164 + .../chaincode-go/collections_config.json | 35 + .../chaincode-go/go.mod | 25 + .../chaincode-go/go.sum | 234 ++ .../chaincode-go/main.go | 23 + .../chaincode-java/.gitattributes | 6 + .../assetCollection/indexes/indexOwner.json | 11 + .../chaincode-java/build.gradle | 62 + .../chaincode-java/collections_config.json | 35 + .../config/checkstyle/checkstyle.xml | 171 + .../config/checkstyle/suppressions.xml | 8 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../chaincode-java/gradlew | 188 ++ .../chaincode-java/gradlew.bat | 100 + .../chaincode-java/settings.gradle | 5 + .../fabric/samples/privatedata/Asset.java | 126 + .../privatedata/AssetPrivateDetails.java | 51 + .../samples/privatedata/AssetTransfer.java | 593 ++++ .../privatedata/TransferAgreement.java | 51 + .../privatedata/AssetTransferTest.java | 170 + .../org.mockito.plugins.MockMaker | 1 + asset-transfer-sbe/README.md | 169 + .../application-javascript/.eslintignore | 5 + .../application-javascript/.eslintrc.js | 37 + .../application-javascript/.gitignore | 14 + .../application-javascript/app.js | 349 ++ .../application-javascript/package.json | 16 + .../chaincode-java/.gitattributes | 6 + asset-transfer-sbe/chaincode-java/.gitignore | 10 + .../chaincode-java/build.gradle | 75 + .../config/checkstyle/checkstyle.xml | 172 + .../config/checkstyle/suppressions.xml | 9 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + asset-transfer-sbe/chaincode-java/gradlew | 188 ++ asset-transfer-sbe/chaincode-java/gradlew.bat | 100 + .../chaincode-java/settings.gradle | 5 + .../hyperledger/fabric/samples/sbe/Asset.java | 97 + .../fabric/samples/sbe/AssetContract.java | 255 ++ .../chaincode-typescript/.gitignore | 20 + .../chaincode-typescript/package.json | 65 + .../chaincode-typescript/src/asset.ts | 20 + .../chaincode-typescript/src/assetContract.ts | 152 + .../chaincode-typescript/src/index.ts | 8 + .../chaincode-typescript/tsconfig.json | 18 + .../chaincode-typescript/tslint.json | 21 + .../application-javascript/.eslintignore | 5 + .../application-javascript/.eslintrc.js | 37 + .../application-javascript/.gitignore | 14 + .../application-javascript/app.js | 567 ++++ .../application-javascript/package.json | 16 + .../chaincode-go/README.md | 1 + .../chaincode-go/asset_transfer.go | 556 ++++ .../chaincode-go/asset_transfer_queries.go | 176 + .../chaincode-go/go.mod | 9 + .../chaincode-go/go.sum | 136 + auction/README.md | 416 +++ auction/application-javascript/bid.js | 112 + .../application-javascript/closeAuction.js | 109 + .../application-javascript/createAuction.js | 93 + 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 | 487 +++ .../smart-contract/auctionQueries.go | 148 + auction/chaincode-go/smart-contract/utils.go | 107 + auction/chaincode-go/smartContract.go | 23 + chaincode/README.md | 20 + chaincode/abstore/go/abstore.go | 135 + chaincode/abstore/go/go.mod | 5 + chaincode/abstore/go/go.sum | 149 + chaincode/abstore/java/.gitignore | 61 + chaincode/abstore/java/build.gradle | 39 + .../java/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + chaincode/abstore/java/gradlew | 188 ++ chaincode/abstore/java/gradlew.bat | 100 + chaincode/abstore/java/settings.gradle | 7 + .../hyperledger/fabric-samples/ABstore.java | 136 + chaincode/abstore/javascript/.gitignore | 77 + chaincode/abstore/javascript/abstore.js | 138 + chaincode/abstore/javascript/package.json | 17 + chaincode/fabcar/external/.dockerignore | 5 + chaincode/fabcar/external/.gitignore | 4 + chaincode/fabcar/external/Dockerfile | 17 + chaincode/fabcar/external/README.md | 55 + .../fabcar/external/chaincode.env.example | 8 + chaincode/fabcar/external/fabcar.go | 172 + chaincode/fabcar/external/go.mod | 8 + chaincode/fabcar/external/go.sum | 146 + chaincode/fabcar/external/metadata.json | 4 + chaincode/fabcar/go/fabcar.go | 396 +++ chaincode/fabcar/go/go.mod | 5 + chaincode/fabcar/go/go.sum | 146 + chaincode/fabcar/java/.gitignore | 61 + chaincode/fabcar/java/README.md | 14 + chaincode/fabcar/java/build.gradle | 74 + .../java/config/checkstyle/checkstyle.xml | 178 + .../java/config/checkstyle/suppressions.xml | 9 + .../java/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + chaincode/fabcar/java/gradlew | 188 ++ chaincode/fabcar/java/gradlew.bat | 100 + chaincode/fabcar/java/settings.gradle | 5 + .../fabric/samples/fabcar/Car.java | 79 + .../fabric/samples/fabcar/CarQueryResult.java | 67 + .../fabric/samples/fabcar/FabCar.java | 190 ++ .../samples/fabcar/CarQueryResultTest.java | 93 + .../fabric/samples/fabcar/CarTest.java | 74 + .../fabric/samples/fabcar/FabCarTest.java | 264 ++ chaincode/fabcar/javascript/.editorconfig | 16 + chaincode/fabcar/javascript/.eslintignore | 5 + chaincode/fabcar/javascript/.eslintrc.js | 38 + chaincode/fabcar/javascript/.gitignore | 77 + chaincode/fabcar/javascript/index.js | 12 + chaincode/fabcar/javascript/lib/fabcar.js | 145 + chaincode/fabcar/javascript/package.json | 47 + chaincode/fabcar/typescript/.editorconfig | 16 + chaincode/fabcar/typescript/.gitignore | 81 + chaincode/fabcar/typescript/package.json | 62 + chaincode/fabcar/typescript/src/car.ts | 11 + chaincode/fabcar/typescript/src/fabcar.ts | 140 + chaincode/fabcar/typescript/src/index.ts | 8 + chaincode/fabcar/typescript/tsconfig.json | 16 + chaincode/fabcar/typescript/tslint.json | 21 + .../statedb/couchdb/indexes/indexOwner.json | 1 + chaincode/marbles02/go/go.mod | 12 + chaincode/marbles02/go/go.sum | 85 + chaincode/marbles02/go/marbles_chaincode.go | 755 +++++ chaincode/marbles02/javascript/.gitignore | 77 + .../statedb/couchdb/indexes/indexOwner.json | 1 + .../marbles02/javascript/marbles_chaincode.js | 481 +++ chaincode/marbles02/javascript/package.json | 17 + .../marbles02_private/collections_config.json | 18 + .../collectionMarbles/indexes/indexOwner.json | 1 + chaincode/marbles02_private/go/go.mod | 5 + chaincode/marbles02_private/go/go.sum | 137 + .../go/marbles_chaincode_private.go | 556 ++++ chaincode/sacc/go.mod | 12 + chaincode/sacc/go.sum | 82 + chaincode/sacc/sacc.go | 97 + chaincode/sacc/sacc_test.go | 174 + ci/azure-pipelines.yml | 255 ++ ci/scripts/lint.sh | 51 + ci/scripts/pullFabricImages.sh | 15 + ci/scripts/run-test-network-basic.sh | 65 + ci/scripts/run-test-network-events.sh | 37 + ci/scripts/run-test-network-ledger.sh | 44 + ci/scripts/run-test-network-private.sh | 35 + ci/scripts/run-test-network-sbe.sh | 35 + ci/scripts/run-test-network-secured.sh | 37 + .../commercial-paper/azure-pipelines-go.yml | 97 + .../commercial-paper/azure-pipelines-java.yml | 92 + .../azure-pipelines-javascript.yml | 82 + ci/templates/fabcar/azure-pipelines-go.yml | 21 + ci/templates/fabcar/azure-pipelines-java.yml | 14 + .../fabcar/azure-pipelines-javascript.yml | 21 + .../fabcar/azure-pipelines-typescript.yml | 22 + ci/templates/install-deps.yml | 17 + commercial-paper/.gitignore | 4 + commercial-paper/README.md | 646 ++++ commercial-paper/img/overview.png | Bin 0 -> 122143 bytes commercial-paper/img/transaction-flow.png | Bin 0 -> 21986 bytes commercial-paper/network-clean.sh | 26 + commercial-paper/network-starter.sh | 36 + .../organization/digibank/.gitignore | 1 + .../digibank/application-java/.gitignore | 1 + .../.settings/org.eclipse.jdt.core.prefs | 6 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../digibank/application-java/pom.xml | 96 + .../src/org/digibank/AddToWallet.java | 68 + .../src/org/digibank/Buy.java | 74 + .../src/org/digibank/Redeem.java | 73 + .../src/org/papernet/CommercialPaper.java | 181 ++ .../src/org/papernet/ledgerapi/State.java | 60 + .../digibank/application/.eslintrc.js | 37 + .../digibank/application/.gitignore | 1 + .../digibank/application/addToWallet.js | 55 + .../organization/digibank/application/buy.js | 105 + .../digibank/application/buy_request.js | 105 + .../digibank/application/enrollUser.js | 56 + .../digibank/application/package.json | 20 + .../digibank/application/queryapp.js | 155 + .../digibank/application/redeem.js | 106 + .../configuration/cli/docker-compose.yml | 38 + .../configuration/cli/monitordocker.sh | 31 + .../contract-go/commercial-paper/paper.go | 139 + .../commercial-paper/paper_test.go | 125 + .../commercial-paper/papercontext.go | 35 + .../commercial-paper/papercontext_test.go | 31 + .../commercial-paper/papercontract.go | 96 + .../commercial-paper/papercontract_test.go | 185 ++ .../contract-go/commercial-paper/paperlist.go | 55 + .../commercial-paper/paperlist_test.go | 103 + .../organization/digibank/contract-go/go.mod | 13 + .../organization/digibank/contract-go/go.sum | 167 + .../digibank/contract-go/ledger-api/state.go | 27 + .../contract-go/ledger-api/statelist.go | 61 + .../organization/digibank/contract-go/main.go | 35 + .../digibank/contract-java/.gitignore | 10 + .../digibank/contract-java/build.gradle | 38 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../digibank/contract-java/gradlew | 188 ++ .../digibank/contract-java/gradlew.bat | 100 + .../digibank/contract-java/settings.gradle | 2 + .../contract-java/shadow-build.gradle | 50 + .../java/org/example/CommercialPaper.java | 183 ++ .../org/example/CommercialPaperContext.java | 15 + .../org/example/CommercialPaperContract.java | 170 + .../src/main/java/org/example/PaperList.java | 31 + .../java/org/example/ledgerapi/State.java | 60 + .../example/ledgerapi/StateDeserializer.java | 6 + .../java/org/example/ledgerapi/StateList.java | 48 + .../example/ledgerapi/impl/StateListImpl.java | 100 + .../digibank/contract/.editorconfig | 16 + .../digibank/contract/.eslintignore | 5 + .../digibank/contract/.eslintrc.js | 37 + .../organization/digibank/contract/.npmignore | 77 + .../organization/digibank/contract/index.js | 10 + .../digibank/contract/ledger-api/state.js | 105 + .../digibank/contract/ledger-api/statelist.js | 74 + .../digibank/contract/lib/paper.js | 121 + .../digibank/contract/lib/papercontract.js | 339 ++ .../digibank/contract/lib/paperlist.js | 35 + .../digibank/contract/lib/queries.js | 215 ++ .../digibank/contract/package.json | 49 + .../digibank/contract/test/contract.js | 43 + .../organization/digibank/digibank.sh | 39 + .../organization/digibank/gateway/.gitkeep | 0 .../organization/magnetocorp/.gitignore | 1 + .../magnetocorp/application-java/.gitignore | 1 + .../org.eclipse.core.resources.prefs | 3 + .../.settings/org.eclipse.jdt.core.prefs | 6 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../magnetocorp/application-java/pom.xml | 96 + .../src/org/magnetocorp/AddToWallet.java | 68 + .../src/org/magnetocorp/Issue.java | 75 + .../src/org/papernet/CommercialPaper.java | 183 ++ .../src/org/papernet/ledgerapi/State.java | 60 + .../magnetocorp/application/.eslintrc.js | 37 + .../magnetocorp/application/.gitignore | 1 + .../magnetocorp/application/addToWallet.js | 55 + .../magnetocorp/application/cpListener.js | 97 + .../magnetocorp/application/enrollUser.js | 56 + .../magnetocorp/application/issue.js | 103 + .../magnetocorp/application/package.json | 20 + .../magnetocorp/application/transfer.js | 103 + .../configuration/cli/docker-compose.yml | 37 + .../configuration/cli/monitordocker.sh | 31 + .../contract-go/commercial-paper/paper.go | 139 + .../commercial-paper/paper_test.go | 125 + .../commercial-paper/papercontext.go | 35 + .../commercial-paper/papercontext_test.go | 31 + .../commercial-paper/papercontract.go | 96 + .../commercial-paper/papercontract_test.go | 185 ++ .../contract-go/commercial-paper/paperlist.go | 55 + .../commercial-paper/paperlist_test.go | 103 + .../magnetocorp/contract-go/go.mod | 13 + .../magnetocorp/contract-go/go.sum | 167 + .../contract-go/ledger-api/state.go | 27 + .../contract-go/ledger-api/statelist.go | 61 + .../magnetocorp/contract-go/main.go | 35 + .../org.eclipse.buildship.core.prefs | 2 + .../magnetocorp/contract-java/build.gradle | 50 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../magnetocorp/contract-java/gradlew | 188 ++ .../magnetocorp/contract-java/gradlew.bat | 100 + .../magnetocorp/contract-java/settings.gradle | 2 + .../java/org/example/CommercialPaper.java | 183 ++ .../org/example/CommercialPaperContext.java | 15 + .../org/example/CommercialPaperContract.java | 170 + .../src/main/java/org/example/PaperList.java | 31 + .../java/org/example/ledgerapi/State.java | 60 + .../example/ledgerapi/StateDeserializer.java | 6 + .../java/org/example/ledgerapi/StateList.java | 48 + .../example/ledgerapi/impl/StateListImpl.java | 100 + .../magnetocorp/contract/.editorconfig | 16 + .../magnetocorp/contract/.eslintignore | 5 + .../magnetocorp/contract/.eslintrc.js | 37 + .../magnetocorp/contract/.npmignore | 77 + .../magnetocorp/contract/index.js | 10 + .../magnetocorp/contract/ledger-api/state.js | 105 + .../contract/ledger-api/statelist.js | 74 + .../magnetocorp/contract/lib/paper.js | 121 + .../magnetocorp/contract/lib/papercontract.js | 339 ++ .../magnetocorp/contract/lib/paperlist.js | 35 + .../magnetocorp/contract/lib/queries.js | 215 ++ .../magnetocorp/contract/package.json | 49 + .../magnetocorp/contract/test/contract.js | 43 + .../organization/magnetocorp/gateway/.gitkeep | 0 .../organization/magnetocorp/magnetocorp.sh | 38 + .../organization/magnetocorp/t.js | 1 + fabcar/.gitignore | 3 + fabcar/go/.gitignore | 2 + fabcar/go/fabcar.go | 141 + fabcar/go/go.mod | 5 + fabcar/go/go.sum | 126 + fabcar/go/runfabcar.sh | 15 + fabcar/java/.gitignore | 7 + fabcar/java/pom.xml | 85 + .../src/main/java/org/example/ClientApp.java | 56 + .../main/java/org/example/EnrollAdmin.java | 55 + .../main/java/org/example/RegisterUser.java | 111 + .../src/test/java/org/example/ClientTest.java | 17 + fabcar/javascript/.editorconfig | 16 + fabcar/javascript/.eslintignore | 5 + fabcar/javascript/.eslintrc.js | 37 + fabcar/javascript/.gitignore | 80 + fabcar/javascript/EncryptDecrypt-v2.js | 274 ++ fabcar/javascript/app.js | 142 + fabcar/javascript/bob.txt | 1 + fabcar/javascript/controllers/error.js | 7 + fabcar/javascript/controllers/index.js | 278 ++ fabcar/javascript/controllers/userShowReq.js | 417 +++ .../javascript/controllers/userUpDownDel.js | 393 +++ fabcar/javascript/database.js | 28 + fabcar/javascript/encryption-decryption.js | 145 + fabcar/javascript/enrollAdmin.js | 56 + fabcar/javascript/invoke.js | 57 + fabcar/javascript/keys/privateKey.key | 30 + fabcar/javascript/keys/publicKey.key | 8 + fabcar/javascript/middleware/is-auth.js | 7 + fabcar/javascript/models/file.js | 29 + fabcar/javascript/models/keyword_index.js | 19 + fabcar/javascript/models/user.js | 77 + fabcar/javascript/package.json | 60 + fabcar/javascript/public/css/forms.css | 32 + fabcar/javascript/public/css/main.css | 271 ++ fabcar/javascript/public/css/user.css | 30 + .../2021-05-30T09:10:32.248Z-*-Thank Bob.txt | Bin 0 -> 256 bytes .../2021-05-30T15:26:42.220Z-*-Angry Bob.txt | Bin 0 -> 256 bytes .../2021-05-30T20:15:31.073Z-*-BobNew1.txt | Bin 0 -> 256 bytes .../2021-05-31T05:16:11.316Z-*-Management.txt | Bin 0 -> 256 bytes ...-05-31T05:18:32.110Z-*-Management-Repy.txt | Bin 0 -> 256 bytes .../files/2021-05-31T14:18:08.451Z-*-test.txt | Bin 0 -> 256 bytes .../2021-05-31T14:43:36.269Z-*-Gupi File1.txt | Bin 0 -> 256 bytes .../2021-05-31T14:46:10.484Z-*-BaghaFile1.txt | Bin 0 -> 256 bytes .../files/2021-07-03T21:41:24.262Z-*-bob.txt | 1 + .../2021-07-04T11:04:16.743Z-*-random.txt | 2 + fabcar/javascript/query.js | 58 + fabcar/javascript/registerUser.js | 75 + fabcar/javascript/rosalind.txt | Bin 0 -> 256 bytes fabcar/javascript/routes/index.js | 74 + fabcar/javascript/routes/user.js | 45 + .../javascript/temp-file/againEncryptFile.txt | 1 + fabcar/javascript/temp-file/decryptedFile.txt | 1 + fabcar/javascript/util.js | 57 + fabcar/javascript/views/404.ejs | 10 + fabcar/javascript/views/auth/login.ejs | 32 + fabcar/javascript/views/auth/signup.ejs | 45 + fabcar/javascript/views/form.ejs | 36 + fabcar/javascript/views/includes/end.ejs | 4 + fabcar/javascript/views/includes/head.ejs | 9 + .../javascript/views/includes/navigation.ejs | 86 + fabcar/javascript/views/index.ejs | 16 + fabcar/javascript/views/updown/download.ejs | 20 + fabcar/javascript/views/updown/downloaded.ejs | 14 + .../javascript/views/updown/notification.ejs | 55 + fabcar/javascript/views/updown/request.ejs | 45 + .../javascript/views/updown/searchResult.ejs | 52 + .../views/updown/showDecryptedContent.ejs | 17 + fabcar/javascript/views/updown/showFile.ejs | 12 + fabcar/javascript/views/updown/upload.ejs | 34 + fabcar/javascript/views/updown/uploaded.ejs | 38 + fabcar/javascript/views/users.ejs | 14 + fabcar/networkDown.sh | 18 + fabcar/startFabric.sh | 128 + fabcar/testRun.sh | 6 + fabcar/typescript/.editorconfig | 16 + fabcar/typescript/.gitignore | 83 + fabcar/typescript/package.json | 59 + fabcar/typescript/src/enrollAdmin.ts | 52 + fabcar/typescript/src/invoke.ts | 53 + fabcar/typescript/src/query.ts | 51 + fabcar/typescript/src/registerUser.ts | 64 + fabcar/typescript/tsconfig.json | 16 + fabcar/typescript/tslint.json | 22 + high-throughput/README.md | 197 ++ 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 | 226 ++ high-throughput/chaincode-go/go.mod | 12 + high-throughput/chaincode-go/go.sum | 86 + .../chaincode-go/high-throughput.go | 396 +++ high-throughput/networkDown.sh | 17 + high-throughput/startFabric.sh | 28 + interest_rate_swaps/README.md | 185 ++ interest_rate_swaps/chaincode/chaincode.go | 313 ++ interest_rate_swaps/chaincode/go.mod | 12 + interest_rate_swaps/chaincode/go.sum | 82 + interest_rate_swaps/network/.gitignore | 2 + interest_rate_swaps/network/configtx.yaml | 454 +++ .../network/crypto-config.yaml | 51 + .../network/docker-compose.yaml | 161 + interest_rate_swaps/network/network.sh | 242 ++ .../network/scripts/check-commit-readiness.sh | 38 + interest_rate_swaps/network/scripts/script.sh | 196 ++ off_chain_data/README.md | 328 ++ off_chain_data/addAssets.js | 118 + off_chain_data/blockEventListener.js | 186 ++ off_chain_data/blockProcessing.js | 216 ++ off_chain_data/config.json | 7 + off_chain_data/couchdbutil.js | 111 + off_chain_data/deleteAsset.js | 69 + off_chain_data/enrollAdmin.js | 55 + off_chain_data/network-clean.sh | 20 + off_chain_data/package.json | 45 + off_chain_data/registerUser.js | 75 + off_chain_data/startFabric.sh | 24 + off_chain_data/transferAsset.js | 69 + scripts/changelog.sh | 15 + test-application/javascript/AppUtil.js | 66 + test-application/javascript/CAUtil.js | 98 + test-network/.env | 3 + test-network/.gitignore | 14 + test-network/README.md | 5 + test-network/addOrg3/.env | 2 + test-network/addOrg3/README.md | 28 + test-network/addOrg3/addOrg3.sh | 284 ++ test-network/addOrg3/ccp-generate.sh | 36 + test-network/addOrg3/ccp-template.json | 49 + test-network/addOrg3/ccp-template.yaml | 34 + test-network/addOrg3/configtx.yaml | 38 + .../docker/docker-compose-ca-org3.yaml | 22 + .../docker/docker-compose-couch-org3.yaml | 39 + .../addOrg3/docker/docker-compose-org3.yaml | 52 + .../org3/fabric-ca-server-config.yaml | 406 +++ .../addOrg3/fabric-ca/registerEnroll.sh | 87 + test-network/addOrg3/org3-crypto.yaml | 21 + test-network/configtx/configtx.yaml | 326 ++ test-network/docker/docker-compose-ca.yaml | 59 + test-network/docker/docker-compose-couch.yaml | 64 + .../docker/docker-compose-test-net.yaml | 147 + test-network/network.sh | 522 +++ test-network/organizations/ccp-generate.sh | 45 + test-network/organizations/ccp-template.json | 49 + test-network/organizations/ccp-template.yaml | 35 + .../cryptogen/crypto-config-orderer.yaml | 22 + .../cryptogen/crypto-config-org1.yaml | 61 + .../cryptogen/crypto-config-org2.yaml | 61 + .../organizations/fabric-ca/registerEnroll.sh | 226 ++ test-network/scripts/configUpdate.sh | 61 + test-network/scripts/createChannel.sh | 98 + test-network/scripts/deployCC.sh | 328 ++ test-network/scripts/envVar.sh | 100 + .../scripts/org3-scripts/joinChannel.sh | 70 + .../org3-scripts/updateChannelConfig.sh | 52 + test-network/scripts/setAnchorPeer.sh | 58 + test-network/scripts/utils.sh | 156 + token-erc-20/README.md | 403 +++ .../chaincode-go/chaincode/token_contract.go | 479 +++ token-erc-20/chaincode-go/go.mod | 5 + token-erc-20/chaincode-go/go.sum | 145 + token-erc-20/chaincode-go/token_erc_20.go | 23 + .../chaincode-javascript/.editorconfig | 16 + .../chaincode-javascript/.eslintignore | 5 + .../chaincode-javascript/.eslintrc.js | 39 + token-erc-20/chaincode-javascript/.gitignore | 78 + token-erc-20/chaincode-javascript/index.js | 10 + .../chaincode-javascript/lib/tokenERC20.js | 409 +++ .../chaincode-javascript/package.json | 51 + .../test/tokenERC20.test.js | 281 ++ token-utxo/README.md | 212 ++ .../chaincode-go/chaincode/token_contract.go | 221 ++ token-utxo/chaincode-go/go.mod | 5 + token-utxo/chaincode-go/go.sum | 145 + token-utxo/chaincode-go/token_utxo.go | 23 + 636 files changed, 61723 insertions(+) create mode 100644 .github/settings.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 MAINTAINERS.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 asset-transfer-abac/README.md create mode 100644 asset-transfer-abac/chaincode-go/go.mod create mode 100644 asset-transfer-abac/chaincode-go/go.sum create mode 100644 asset-transfer-abac/chaincode-go/smart-contract/abac.go create mode 100644 asset-transfer-abac/chaincode-go/smartContract.go create mode 100755 asset-transfer-basic/.gitignore create mode 100755 asset-transfer-basic/application-go/.gitignore create mode 100644 asset-transfer-basic/application-go/assetTransfer.go create mode 100644 asset-transfer-basic/application-go/go.mod create mode 100644 asset-transfer-basic/application-go/go.sum create mode 100644 asset-transfer-basic/application-java/.gitattributes create mode 100644 asset-transfer-basic/application-java/build.gradle create mode 100644 asset-transfer-basic/application-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 asset-transfer-basic/application-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 asset-transfer-basic/application-java/gradlew create mode 100644 asset-transfer-basic/application-java/gradlew.bat create mode 100644 asset-transfer-basic/application-java/settings.gradle create mode 100644 asset-transfer-basic/application-java/src/main/java/application/java/App.java create mode 100644 asset-transfer-basic/application-java/src/main/java/application/java/EnrollAdmin.java create mode 100644 asset-transfer-basic/application-java/src/main/java/application/java/RegisterUser.java create mode 100644 asset-transfer-basic/application-java/src/main/resources/log4j.properties create mode 100644 asset-transfer-basic/application-javascript/.eslintignore create mode 100644 asset-transfer-basic/application-javascript/.eslintrc.js create mode 100644 asset-transfer-basic/application-javascript/.gitignore create mode 100644 asset-transfer-basic/application-javascript/app.js create mode 100644 asset-transfer-basic/application-javascript/package.json create mode 100644 asset-transfer-basic/application-typescript/.gitignore 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 create mode 100644 asset-transfer-basic/chaincode-external/.dockerignore create mode 100644 asset-transfer-basic/chaincode-external/.gitignore create mode 100644 asset-transfer-basic/chaincode-external/Dockerfile create mode 100755 asset-transfer-basic/chaincode-external/README.md create mode 100644 asset-transfer-basic/chaincode-external/assetTransfer.go create mode 100644 asset-transfer-basic/chaincode-external/chaincode.env create mode 100644 asset-transfer-basic/chaincode-external/connection.json create mode 100644 asset-transfer-basic/chaincode-external/go.mod create mode 100644 asset-transfer-basic/chaincode-external/go.sum create mode 100644 asset-transfer-basic/chaincode-external/metadata.json create mode 100755 asset-transfer-basic/chaincode-external/sampleBuilder/bin/detect create mode 100755 asset-transfer-basic/chaincode-external/sampleBuilder/bin/release create mode 100644 asset-transfer-basic/chaincode-go/assetTransfer.go create mode 100644 asset-transfer-basic/chaincode-go/chaincode/mocks/chaincodestub.go create mode 100644 asset-transfer-basic/chaincode-go/chaincode/mocks/statequeryiterator.go create mode 100644 asset-transfer-basic/chaincode-go/chaincode/mocks/transaction.go create mode 100644 asset-transfer-basic/chaincode-go/chaincode/smartcontract.go create mode 100644 asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go create mode 100644 asset-transfer-basic/chaincode-go/go.mod create mode 100644 asset-transfer-basic/chaincode-go/go.sum create mode 100644 asset-transfer-basic/chaincode-java/.gitattributes create mode 100644 asset-transfer-basic/chaincode-java/build.gradle create mode 100644 asset-transfer-basic/chaincode-java/config/checkstyle/checkstyle.xml create mode 100644 asset-transfer-basic/chaincode-java/config/checkstyle/suppressions.xml create mode 100644 asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 asset-transfer-basic/chaincode-java/gradlew create mode 100644 asset-transfer-basic/chaincode-java/gradlew.bat create mode 100644 asset-transfer-basic/chaincode-java/settings.gradle create mode 100644 asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/Asset.java create mode 100644 asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/AssetTransfer.java create mode 100644 asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTest.java create mode 100644 asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTransferTest.java create mode 100644 asset-transfer-basic/chaincode-javascript/.eslintignore create mode 100644 asset-transfer-basic/chaincode-javascript/.eslintrc.js create mode 100644 asset-transfer-basic/chaincode-javascript/.gitignore create mode 100644 asset-transfer-basic/chaincode-javascript/index.js create mode 100644 asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js create mode 100644 asset-transfer-basic/chaincode-javascript/package.json create mode 100644 asset-transfer-basic/chaincode-javascript/test/assetTransfer.test.js create mode 100644 asset-transfer-basic/chaincode-typescript/.gitignore create mode 100644 asset-transfer-basic/chaincode-typescript/package.json create mode 100644 asset-transfer-basic/chaincode-typescript/src/asset.ts create mode 100644 asset-transfer-basic/chaincode-typescript/src/assetTransfer.ts create mode 100644 asset-transfer-basic/chaincode-typescript/src/index.ts create mode 100644 asset-transfer-basic/chaincode-typescript/tsconfig.json create mode 100644 asset-transfer-basic/chaincode-typescript/tslint.json create mode 100644 asset-transfer-events/README.md 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-java/build.gradle create mode 100644 asset-transfer-events/chaincode-java/config/checkstyle/checkstyle.xml create mode 100644 asset-transfer-events/chaincode-java/config/checkstyle/suppressions.xml create mode 100644 asset-transfer-events/chaincode-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 asset-transfer-events/chaincode-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 asset-transfer-events/chaincode-java/gradlew create mode 100644 asset-transfer-events/chaincode-java/gradlew.bat create mode 100644 asset-transfer-events/chaincode-java/settings.gradle create mode 100644 asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/Asset.java create mode 100644 asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/AssetTransfer.java 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 100644 asset-transfer-ledger-queries/application-java/.gitattributes create mode 100644 asset-transfer-ledger-queries/application-java/build.gradle create mode 100644 asset-transfer-ledger-queries/application-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 asset-transfer-ledger-queries/application-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 asset-transfer-ledger-queries/application-java/gradlew create mode 100644 asset-transfer-ledger-queries/application-java/gradlew.bat create mode 100644 asset-transfer-ledger-queries/application-java/settings.gradle create mode 100644 asset-transfer-ledger-queries/application-java/src/main/java/application/java/App.java create mode 100644 asset-transfer-ledger-queries/application-java/src/main/java/application/java/EnrollAdmin.java create mode 100644 asset-transfer-ledger-queries/application-java/src/main/java/application/java/RegisterUser.java create mode 100644 asset-transfer-ledger-queries/application-java/src/main/resources/log4j.properties create mode 100644 asset-transfer-ledger-queries/application-javascript/.eslintignore create mode 100644 asset-transfer-ledger-queries/application-javascript/.eslintrc.js create mode 100644 asset-transfer-ledger-queries/application-javascript/.gitignore create mode 100644 asset-transfer-ledger-queries/application-javascript/app.js create mode 100644 asset-transfer-ledger-queries/application-javascript/package.json create mode 100644 asset-transfer-ledger-queries/chaincode-go/META-INF/statedb/couchdb/indexes/indexOwner.json create mode 100644 asset-transfer-ledger-queries/chaincode-go/asset_transfer_ledger_chaincode.go create mode 100644 asset-transfer-ledger-queries/chaincode-go/go.mod create mode 100644 asset-transfer-ledger-queries/chaincode-go/go.sum create mode 100644 asset-transfer-ledger-queries/chaincode-javascript/.eslintignore create mode 100644 asset-transfer-ledger-queries/chaincode-javascript/.eslintrc.js create mode 100644 asset-transfer-ledger-queries/chaincode-javascript/index.js create mode 100644 asset-transfer-ledger-queries/chaincode-javascript/lib/META-INF/statedb/couchdb/indexes/indexOwner.json create mode 100644 asset-transfer-ledger-queries/chaincode-javascript/lib/asset_transfer_ledger_chaincode.js create mode 100644 asset-transfer-ledger-queries/chaincode-javascript/package.json create mode 100644 asset-transfer-private-data/application-javascript/.eslintignore create mode 100644 asset-transfer-private-data/application-javascript/.eslintrc.js create mode 100644 asset-transfer-private-data/application-javascript/.gitignore create mode 100644 asset-transfer-private-data/application-javascript/app.js create mode 100644 asset-transfer-private-data/application-javascript/package.json create mode 100644 asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json create mode 100644 asset-transfer-private-data/chaincode-go/README.md create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/asset_queries.go create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/asset_queries_test.go create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/asset_transfer.go create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/asset_transfer_test.go create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/mocks/chaincodestub.go create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/mocks/clientIdentity.go create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/mocks/statequeryiterator.go create mode 100644 asset-transfer-private-data/chaincode-go/chaincode/mocks/transaction.go create mode 100644 asset-transfer-private-data/chaincode-go/collections_config.json create mode 100644 asset-transfer-private-data/chaincode-go/go.mod create mode 100644 asset-transfer-private-data/chaincode-go/go.sum create mode 100644 asset-transfer-private-data/chaincode-go/main.go create mode 100644 asset-transfer-private-data/chaincode-java/.gitattributes create mode 100644 asset-transfer-private-data/chaincode-java/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json create mode 100644 asset-transfer-private-data/chaincode-java/build.gradle create mode 100644 asset-transfer-private-data/chaincode-java/collections_config.json create mode 100644 asset-transfer-private-data/chaincode-java/config/checkstyle/checkstyle.xml create mode 100644 asset-transfer-private-data/chaincode-java/config/checkstyle/suppressions.xml create mode 100644 asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 asset-transfer-private-data/chaincode-java/gradlew create mode 100644 asset-transfer-private-data/chaincode-java/gradlew.bat create mode 100644 asset-transfer-private-data/chaincode-java/settings.gradle create mode 100644 asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/Asset.java create mode 100644 asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetPrivateDetails.java create mode 100644 asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetTransfer.java create mode 100644 asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/TransferAgreement.java create mode 100644 asset-transfer-private-data/chaincode-java/src/test/java/org/hyperledger/fabric/samples/privatedata/AssetTransferTest.java create mode 100644 asset-transfer-private-data/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 asset-transfer-sbe/README.md create mode 100644 asset-transfer-sbe/application-javascript/.eslintignore create mode 100644 asset-transfer-sbe/application-javascript/.eslintrc.js create mode 100644 asset-transfer-sbe/application-javascript/.gitignore create mode 100644 asset-transfer-sbe/application-javascript/app.js create mode 100644 asset-transfer-sbe/application-javascript/package.json create mode 100644 asset-transfer-sbe/chaincode-java/.gitattributes create mode 100644 asset-transfer-sbe/chaincode-java/.gitignore create mode 100644 asset-transfer-sbe/chaincode-java/build.gradle create mode 100644 asset-transfer-sbe/chaincode-java/config/checkstyle/checkstyle.xml create mode 100644 asset-transfer-sbe/chaincode-java/config/checkstyle/suppressions.xml create mode 100644 asset-transfer-sbe/chaincode-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 asset-transfer-sbe/chaincode-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 asset-transfer-sbe/chaincode-java/gradlew create mode 100644 asset-transfer-sbe/chaincode-java/gradlew.bat create mode 100644 asset-transfer-sbe/chaincode-java/settings.gradle create mode 100644 asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/Asset.java create mode 100644 asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/AssetContract.java create mode 100644 asset-transfer-sbe/chaincode-typescript/.gitignore create mode 100644 asset-transfer-sbe/chaincode-typescript/package.json create mode 100644 asset-transfer-sbe/chaincode-typescript/src/asset.ts create mode 100644 asset-transfer-sbe/chaincode-typescript/src/assetContract.ts create mode 100644 asset-transfer-sbe/chaincode-typescript/src/index.ts create mode 100644 asset-transfer-sbe/chaincode-typescript/tsconfig.json create mode 100644 asset-transfer-sbe/chaincode-typescript/tslint.json create mode 100644 asset-transfer-secured-agreement/application-javascript/.eslintignore create mode 100644 asset-transfer-secured-agreement/application-javascript/.eslintrc.js create mode 100644 asset-transfer-secured-agreement/application-javascript/.gitignore create mode 100644 asset-transfer-secured-agreement/application-javascript/app.js create mode 100644 asset-transfer-secured-agreement/application-javascript/package.json create mode 100644 asset-transfer-secured-agreement/chaincode-go/README.md create mode 100644 asset-transfer-secured-agreement/chaincode-go/asset_transfer.go create mode 100644 asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go create mode 100644 asset-transfer-secured-agreement/chaincode-go/go.mod create mode 100644 asset-transfer-secured-agreement/chaincode-go/go.sum 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 chaincode/README.md create mode 100644 chaincode/abstore/go/abstore.go create mode 100644 chaincode/abstore/go/go.mod create mode 100644 chaincode/abstore/go/go.sum create mode 100644 chaincode/abstore/java/.gitignore create mode 100644 chaincode/abstore/java/build.gradle create mode 100644 chaincode/abstore/java/gradle/wrapper/gradle-wrapper.jar create mode 100644 chaincode/abstore/java/gradle/wrapper/gradle-wrapper.properties create mode 100755 chaincode/abstore/java/gradlew create mode 100644 chaincode/abstore/java/gradlew.bat create mode 100644 chaincode/abstore/java/settings.gradle create mode 100644 chaincode/abstore/java/src/main/java/org/hyperledger/fabric-samples/ABstore.java create mode 100644 chaincode/abstore/javascript/.gitignore create mode 100644 chaincode/abstore/javascript/abstore.js create mode 100644 chaincode/abstore/javascript/package.json create mode 100644 chaincode/fabcar/external/.dockerignore create mode 100644 chaincode/fabcar/external/.gitignore create mode 100644 chaincode/fabcar/external/Dockerfile create mode 100644 chaincode/fabcar/external/README.md create mode 100644 chaincode/fabcar/external/chaincode.env.example create mode 100644 chaincode/fabcar/external/fabcar.go create mode 100644 chaincode/fabcar/external/go.mod create mode 100644 chaincode/fabcar/external/go.sum create mode 100644 chaincode/fabcar/external/metadata.json create mode 100644 chaincode/fabcar/go/fabcar.go create mode 100644 chaincode/fabcar/go/go.mod create mode 100644 chaincode/fabcar/go/go.sum create mode 100644 chaincode/fabcar/java/.gitignore create mode 100644 chaincode/fabcar/java/README.md create mode 100644 chaincode/fabcar/java/build.gradle create mode 100644 chaincode/fabcar/java/config/checkstyle/checkstyle.xml create mode 100644 chaincode/fabcar/java/config/checkstyle/suppressions.xml create mode 100644 chaincode/fabcar/java/gradle/wrapper/gradle-wrapper.jar create mode 100644 chaincode/fabcar/java/gradle/wrapper/gradle-wrapper.properties create mode 100755 chaincode/fabcar/java/gradlew create mode 100644 chaincode/fabcar/java/gradlew.bat create mode 100644 chaincode/fabcar/java/settings.gradle create mode 100644 chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/Car.java create mode 100644 chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/CarQueryResult.java create mode 100644 chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/FabCar.java create mode 100644 chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarQueryResultTest.java create mode 100644 chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarTest.java create mode 100644 chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/FabCarTest.java create mode 100755 chaincode/fabcar/javascript/.editorconfig create mode 100644 chaincode/fabcar/javascript/.eslintignore create mode 100644 chaincode/fabcar/javascript/.eslintrc.js create mode 100644 chaincode/fabcar/javascript/.gitignore create mode 100644 chaincode/fabcar/javascript/index.js create mode 100644 chaincode/fabcar/javascript/lib/fabcar.js create mode 100644 chaincode/fabcar/javascript/package.json create mode 100755 chaincode/fabcar/typescript/.editorconfig create mode 100644 chaincode/fabcar/typescript/.gitignore create mode 100644 chaincode/fabcar/typescript/package.json create mode 100644 chaincode/fabcar/typescript/src/car.ts create mode 100644 chaincode/fabcar/typescript/src/fabcar.ts create mode 100644 chaincode/fabcar/typescript/src/index.ts create mode 100644 chaincode/fabcar/typescript/tsconfig.json create mode 100644 chaincode/fabcar/typescript/tslint.json create mode 100644 chaincode/marbles02/go/META-INF/statedb/couchdb/indexes/indexOwner.json create mode 100644 chaincode/marbles02/go/go.mod create mode 100644 chaincode/marbles02/go/go.sum create mode 100644 chaincode/marbles02/go/marbles_chaincode.go create mode 100644 chaincode/marbles02/javascript/.gitignore create mode 100644 chaincode/marbles02/javascript/META-INF/statedb/couchdb/indexes/indexOwner.json create mode 100644 chaincode/marbles02/javascript/marbles_chaincode.js create mode 100644 chaincode/marbles02/javascript/package.json create mode 100644 chaincode/marbles02_private/collections_config.json create mode 100644 chaincode/marbles02_private/go/META-INF/statedb/couchdb/collections/collectionMarbles/indexes/indexOwner.json create mode 100644 chaincode/marbles02_private/go/go.mod create mode 100644 chaincode/marbles02_private/go/go.sum create mode 100644 chaincode/marbles02_private/go/marbles_chaincode_private.go create mode 100644 chaincode/sacc/go.mod create mode 100644 chaincode/sacc/go.sum create mode 100644 chaincode/sacc/sacc.go create mode 100644 chaincode/sacc/sacc_test.go create mode 100644 ci/azure-pipelines.yml create mode 100755 ci/scripts/lint.sh create mode 100755 ci/scripts/pullFabricImages.sh create mode 100755 ci/scripts/run-test-network-basic.sh create mode 100755 ci/scripts/run-test-network-events.sh create mode 100755 ci/scripts/run-test-network-ledger.sh create mode 100755 ci/scripts/run-test-network-private.sh create mode 100755 ci/scripts/run-test-network-sbe.sh create mode 100755 ci/scripts/run-test-network-secured.sh create mode 100644 ci/templates/commercial-paper/azure-pipelines-go.yml create mode 100644 ci/templates/commercial-paper/azure-pipelines-java.yml create mode 100644 ci/templates/commercial-paper/azure-pipelines-javascript.yml create mode 100644 ci/templates/fabcar/azure-pipelines-go.yml create mode 100644 ci/templates/fabcar/azure-pipelines-java.yml create mode 100644 ci/templates/fabcar/azure-pipelines-javascript.yml create mode 100644 ci/templates/fabcar/azure-pipelines-typescript.yml create mode 100644 ci/templates/install-deps.yml create mode 100644 commercial-paper/.gitignore create mode 100644 commercial-paper/README.md create mode 100644 commercial-paper/img/overview.png create mode 100644 commercial-paper/img/transaction-flow.png create mode 100755 commercial-paper/network-clean.sh create mode 100755 commercial-paper/network-starter.sh create mode 100644 commercial-paper/organization/digibank/.gitignore create mode 100644 commercial-paper/organization/digibank/application-java/.gitignore create mode 100644 commercial-paper/organization/digibank/application-java/.settings/org.eclipse.jdt.core.prefs create mode 100644 commercial-paper/organization/digibank/application-java/.settings/org.eclipse.m2e.core.prefs create mode 100644 commercial-paper/organization/digibank/application-java/pom.xml create mode 100644 commercial-paper/organization/digibank/application-java/src/org/digibank/AddToWallet.java create mode 100644 commercial-paper/organization/digibank/application-java/src/org/digibank/Buy.java create mode 100644 commercial-paper/organization/digibank/application-java/src/org/digibank/Redeem.java create mode 100644 commercial-paper/organization/digibank/application-java/src/org/papernet/CommercialPaper.java create mode 100644 commercial-paper/organization/digibank/application-java/src/org/papernet/ledgerapi/State.java create mode 100644 commercial-paper/organization/digibank/application/.eslintrc.js create mode 100644 commercial-paper/organization/digibank/application/.gitignore create mode 100644 commercial-paper/organization/digibank/application/addToWallet.js create mode 100644 commercial-paper/organization/digibank/application/buy.js create mode 100644 commercial-paper/organization/digibank/application/buy_request.js create mode 100644 commercial-paper/organization/digibank/application/enrollUser.js create mode 100644 commercial-paper/organization/digibank/application/package.json create mode 100644 commercial-paper/organization/digibank/application/queryapp.js create mode 100644 commercial-paper/organization/digibank/application/redeem.js create mode 100644 commercial-paper/organization/digibank/configuration/cli/docker-compose.yml create mode 100755 commercial-paper/organization/digibank/configuration/cli/monitordocker.sh create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/paper.go create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/paper_test.go create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext.go create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext_test.go create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract.go create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract_test.go create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist.go create mode 100644 commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist_test.go create mode 100644 commercial-paper/organization/digibank/contract-go/go.mod create mode 100644 commercial-paper/organization/digibank/contract-go/go.sum create mode 100644 commercial-paper/organization/digibank/contract-go/ledger-api/state.go create mode 100644 commercial-paper/organization/digibank/contract-go/ledger-api/statelist.go create mode 100644 commercial-paper/organization/digibank/contract-go/main.go create mode 100644 commercial-paper/organization/digibank/contract-java/.gitignore create mode 100644 commercial-paper/organization/digibank/contract-java/build.gradle create mode 100644 commercial-paper/organization/digibank/contract-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 commercial-paper/organization/digibank/contract-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 commercial-paper/organization/digibank/contract-java/gradlew create mode 100644 commercial-paper/organization/digibank/contract-java/gradlew.bat create mode 100644 commercial-paper/organization/digibank/contract-java/settings.gradle create mode 100644 commercial-paper/organization/digibank/contract-java/shadow-build.gradle create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaper.java create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContext.java create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContract.java create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/PaperList.java create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/State.java create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateList.java create mode 100644 commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java create mode 100755 commercial-paper/organization/digibank/contract/.editorconfig create mode 100644 commercial-paper/organization/digibank/contract/.eslintignore create mode 100644 commercial-paper/organization/digibank/contract/.eslintrc.js create mode 100644 commercial-paper/organization/digibank/contract/.npmignore create mode 100644 commercial-paper/organization/digibank/contract/index.js create mode 100644 commercial-paper/organization/digibank/contract/ledger-api/state.js create mode 100644 commercial-paper/organization/digibank/contract/ledger-api/statelist.js create mode 100644 commercial-paper/organization/digibank/contract/lib/paper.js create mode 100644 commercial-paper/organization/digibank/contract/lib/papercontract.js create mode 100644 commercial-paper/organization/digibank/contract/lib/paperlist.js create mode 100644 commercial-paper/organization/digibank/contract/lib/queries.js create mode 100644 commercial-paper/organization/digibank/contract/package.json create mode 100644 commercial-paper/organization/digibank/contract/test/contract.js create mode 100755 commercial-paper/organization/digibank/digibank.sh create mode 100644 commercial-paper/organization/digibank/gateway/.gitkeep create mode 100644 commercial-paper/organization/magnetocorp/.gitignore create mode 100644 commercial-paper/organization/magnetocorp/application-java/.gitignore create mode 100644 commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.core.resources.prefs create mode 100644 commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.jdt.core.prefs create mode 100644 commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.m2e.core.prefs create mode 100644 commercial-paper/organization/magnetocorp/application-java/pom.xml create mode 100644 commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/AddToWallet.java create mode 100644 commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/Issue.java create mode 100644 commercial-paper/organization/magnetocorp/application-java/src/org/papernet/CommercialPaper.java create mode 100644 commercial-paper/organization/magnetocorp/application-java/src/org/papernet/ledgerapi/State.java create mode 100644 commercial-paper/organization/magnetocorp/application/.eslintrc.js create mode 100644 commercial-paper/organization/magnetocorp/application/.gitignore create mode 100644 commercial-paper/organization/magnetocorp/application/addToWallet.js create mode 100644 commercial-paper/organization/magnetocorp/application/cpListener.js create mode 100644 commercial-paper/organization/magnetocorp/application/enrollUser.js create mode 100644 commercial-paper/organization/magnetocorp/application/issue.js create mode 100644 commercial-paper/organization/magnetocorp/application/package.json create mode 100644 commercial-paper/organization/magnetocorp/application/transfer.js create mode 100644 commercial-paper/organization/magnetocorp/configuration/cli/docker-compose.yml create mode 100755 commercial-paper/organization/magnetocorp/configuration/cli/monitordocker.sh create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper_test.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext_test.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract_test.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist_test.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/go.mod create mode 100644 commercial-paper/organization/magnetocorp/contract-go/go.sum create mode 100644 commercial-paper/organization/magnetocorp/contract-go/ledger-api/state.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/ledger-api/statelist.go create mode 100644 commercial-paper/organization/magnetocorp/contract-go/main.go create mode 100644 commercial-paper/organization/magnetocorp/contract-java/.settings/org.eclipse.buildship.core.prefs create mode 100644 commercial-paper/organization/magnetocorp/contract-java/build.gradle create mode 100644 commercial-paper/organization/magnetocorp/contract-java/gradle/wrapper/gradle-wrapper.jar create mode 100644 commercial-paper/organization/magnetocorp/contract-java/gradle/wrapper/gradle-wrapper.properties create mode 100755 commercial-paper/organization/magnetocorp/contract-java/gradlew create mode 100644 commercial-paper/organization/magnetocorp/contract-java/gradlew.bat create mode 100644 commercial-paper/organization/magnetocorp/contract-java/settings.gradle create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaper.java create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContext.java create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContract.java create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/PaperList.java create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/State.java create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateList.java create mode 100644 commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java create mode 100755 commercial-paper/organization/magnetocorp/contract/.editorconfig create mode 100644 commercial-paper/organization/magnetocorp/contract/.eslintignore create mode 100644 commercial-paper/organization/magnetocorp/contract/.eslintrc.js create mode 100644 commercial-paper/organization/magnetocorp/contract/.npmignore create mode 100644 commercial-paper/organization/magnetocorp/contract/index.js create mode 100644 commercial-paper/organization/magnetocorp/contract/ledger-api/state.js create mode 100644 commercial-paper/organization/magnetocorp/contract/ledger-api/statelist.js create mode 100644 commercial-paper/organization/magnetocorp/contract/lib/paper.js create mode 100644 commercial-paper/organization/magnetocorp/contract/lib/papercontract.js create mode 100644 commercial-paper/organization/magnetocorp/contract/lib/paperlist.js create mode 100644 commercial-paper/organization/magnetocorp/contract/lib/queries.js create mode 100644 commercial-paper/organization/magnetocorp/contract/package.json create mode 100644 commercial-paper/organization/magnetocorp/contract/test/contract.js create mode 100644 commercial-paper/organization/magnetocorp/gateway/.gitkeep create mode 100755 commercial-paper/organization/magnetocorp/magnetocorp.sh create mode 100644 commercial-paper/organization/magnetocorp/t.js create mode 100644 fabcar/.gitignore create mode 100755 fabcar/go/.gitignore create mode 100644 fabcar/go/fabcar.go create mode 100644 fabcar/go/go.mod create mode 100644 fabcar/go/go.sum create mode 100755 fabcar/go/runfabcar.sh create mode 100755 fabcar/java/.gitignore create mode 100644 fabcar/java/pom.xml create mode 100755 fabcar/java/src/main/java/org/example/ClientApp.java create mode 100644 fabcar/java/src/main/java/org/example/EnrollAdmin.java create mode 100644 fabcar/java/src/main/java/org/example/RegisterUser.java create mode 100644 fabcar/java/src/test/java/org/example/ClientTest.java create mode 100755 fabcar/javascript/.editorconfig create mode 100644 fabcar/javascript/.eslintignore create mode 100644 fabcar/javascript/.eslintrc.js create mode 100644 fabcar/javascript/.gitignore create mode 100644 fabcar/javascript/EncryptDecrypt-v2.js create mode 100644 fabcar/javascript/app.js create mode 100644 fabcar/javascript/bob.txt create mode 100644 fabcar/javascript/controllers/error.js create mode 100644 fabcar/javascript/controllers/index.js create mode 100644 fabcar/javascript/controllers/userShowReq.js create mode 100644 fabcar/javascript/controllers/userUpDownDel.js create mode 100644 fabcar/javascript/database.js create mode 100644 fabcar/javascript/encryption-decryption.js create mode 100644 fabcar/javascript/enrollAdmin.js create mode 100644 fabcar/javascript/invoke.js create mode 100644 fabcar/javascript/keys/privateKey.key create mode 100644 fabcar/javascript/keys/publicKey.key create mode 100644 fabcar/javascript/middleware/is-auth.js create mode 100644 fabcar/javascript/models/file.js create mode 100644 fabcar/javascript/models/keyword_index.js create mode 100644 fabcar/javascript/models/user.js create mode 100644 fabcar/javascript/package.json create mode 100644 fabcar/javascript/public/css/forms.css create mode 100644 fabcar/javascript/public/css/main.css create mode 100644 fabcar/javascript/public/css/user.css create mode 100644 fabcar/javascript/public/files/2021-05-30T09:10:32.248Z-*-Thank Bob.txt create mode 100644 fabcar/javascript/public/files/2021-05-30T15:26:42.220Z-*-Angry Bob.txt create mode 100644 fabcar/javascript/public/files/2021-05-30T20:15:31.073Z-*-BobNew1.txt create mode 100644 fabcar/javascript/public/files/2021-05-31T05:16:11.316Z-*-Management.txt create mode 100644 fabcar/javascript/public/files/2021-05-31T05:18:32.110Z-*-Management-Repy.txt create mode 100644 fabcar/javascript/public/files/2021-05-31T14:18:08.451Z-*-test.txt create mode 100644 fabcar/javascript/public/files/2021-05-31T14:43:36.269Z-*-Gupi File1.txt create mode 100644 fabcar/javascript/public/files/2021-05-31T14:46:10.484Z-*-BaghaFile1.txt create mode 100644 fabcar/javascript/public/files/2021-07-03T21:41:24.262Z-*-bob.txt create mode 100644 fabcar/javascript/public/files/2021-07-04T11:04:16.743Z-*-random.txt create mode 100644 fabcar/javascript/query.js create mode 100644 fabcar/javascript/registerUser.js create mode 100644 fabcar/javascript/rosalind.txt create mode 100644 fabcar/javascript/routes/index.js create mode 100644 fabcar/javascript/routes/user.js create mode 100644 fabcar/javascript/temp-file/againEncryptFile.txt create mode 100644 fabcar/javascript/temp-file/decryptedFile.txt create mode 100644 fabcar/javascript/util.js create mode 100644 fabcar/javascript/views/404.ejs create mode 100644 fabcar/javascript/views/auth/login.ejs create mode 100644 fabcar/javascript/views/auth/signup.ejs create mode 100644 fabcar/javascript/views/form.ejs create mode 100644 fabcar/javascript/views/includes/end.ejs create mode 100644 fabcar/javascript/views/includes/head.ejs create mode 100644 fabcar/javascript/views/includes/navigation.ejs create mode 100644 fabcar/javascript/views/index.ejs create mode 100644 fabcar/javascript/views/updown/download.ejs create mode 100644 fabcar/javascript/views/updown/downloaded.ejs create mode 100644 fabcar/javascript/views/updown/notification.ejs create mode 100644 fabcar/javascript/views/updown/request.ejs create mode 100644 fabcar/javascript/views/updown/searchResult.ejs create mode 100644 fabcar/javascript/views/updown/showDecryptedContent.ejs create mode 100644 fabcar/javascript/views/updown/showFile.ejs create mode 100644 fabcar/javascript/views/updown/upload.ejs create mode 100644 fabcar/javascript/views/updown/uploaded.ejs create mode 100644 fabcar/javascript/views/users.ejs create mode 100755 fabcar/networkDown.sh create mode 100755 fabcar/startFabric.sh create mode 100755 fabcar/testRun.sh create mode 100755 fabcar/typescript/.editorconfig create mode 100644 fabcar/typescript/.gitignore create mode 100644 fabcar/typescript/package.json create mode 100644 fabcar/typescript/src/enrollAdmin.ts create mode 100644 fabcar/typescript/src/invoke.ts create mode 100644 fabcar/typescript/src/query.ts create mode 100644 fabcar/typescript/src/registerUser.ts create mode 100644 fabcar/typescript/tsconfig.json create mode 100644 fabcar/typescript/tslint.json create mode 100644 high-throughput/README.md 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 create mode 100644 high-throughput/chaincode-go/go.mod create mode 100644 high-throughput/chaincode-go/go.sum create mode 100644 high-throughput/chaincode-go/high-throughput.go create mode 100755 high-throughput/networkDown.sh create mode 100755 high-throughput/startFabric.sh create mode 100644 interest_rate_swaps/README.md create mode 100644 interest_rate_swaps/chaincode/chaincode.go create mode 100644 interest_rate_swaps/chaincode/go.mod create mode 100644 interest_rate_swaps/chaincode/go.sum create mode 100644 interest_rate_swaps/network/.gitignore create mode 100644 interest_rate_swaps/network/configtx.yaml create mode 100644 interest_rate_swaps/network/crypto-config.yaml create mode 100644 interest_rate_swaps/network/docker-compose.yaml create mode 100755 interest_rate_swaps/network/network.sh create mode 100644 interest_rate_swaps/network/scripts/check-commit-readiness.sh create mode 100755 interest_rate_swaps/network/scripts/script.sh create mode 100644 off_chain_data/README.md create mode 100644 off_chain_data/addAssets.js create mode 100644 off_chain_data/blockEventListener.js create mode 100644 off_chain_data/blockProcessing.js create mode 100644 off_chain_data/config.json create mode 100644 off_chain_data/couchdbutil.js create mode 100644 off_chain_data/deleteAsset.js create mode 100644 off_chain_data/enrollAdmin.js create mode 100755 off_chain_data/network-clean.sh create mode 100644 off_chain_data/package.json create mode 100644 off_chain_data/registerUser.js create mode 100755 off_chain_data/startFabric.sh create mode 100644 off_chain_data/transferAsset.js create mode 100755 scripts/changelog.sh create mode 100644 test-application/javascript/AppUtil.js create mode 100644 test-application/javascript/CAUtil.js create mode 100644 test-network/.env create mode 100644 test-network/.gitignore create mode 100644 test-network/README.md create mode 100644 test-network/addOrg3/.env create mode 100644 test-network/addOrg3/README.md create mode 100755 test-network/addOrg3/addOrg3.sh create mode 100755 test-network/addOrg3/ccp-generate.sh create mode 100644 test-network/addOrg3/ccp-template.json create mode 100644 test-network/addOrg3/ccp-template.yaml create mode 100644 test-network/addOrg3/configtx.yaml create mode 100644 test-network/addOrg3/docker/docker-compose-ca-org3.yaml create mode 100644 test-network/addOrg3/docker/docker-compose-couch-org3.yaml create mode 100644 test-network/addOrg3/docker/docker-compose-org3.yaml create mode 100644 test-network/addOrg3/fabric-ca/org3/fabric-ca-server-config.yaml create mode 100644 test-network/addOrg3/fabric-ca/registerEnroll.sh create mode 100644 test-network/addOrg3/org3-crypto.yaml create mode 100644 test-network/configtx/configtx.yaml create mode 100644 test-network/docker/docker-compose-ca.yaml create mode 100644 test-network/docker/docker-compose-couch.yaml create mode 100644 test-network/docker/docker-compose-test-net.yaml create mode 100755 test-network/network.sh create mode 100755 test-network/organizations/ccp-generate.sh create mode 100755 test-network/organizations/ccp-template.json create mode 100755 test-network/organizations/ccp-template.yaml create mode 100755 test-network/organizations/cryptogen/crypto-config-orderer.yaml create mode 100755 test-network/organizations/cryptogen/crypto-config-org1.yaml create mode 100755 test-network/organizations/cryptogen/crypto-config-org2.yaml create mode 100755 test-network/organizations/fabric-ca/registerEnroll.sh create mode 100755 test-network/scripts/configUpdate.sh create mode 100755 test-network/scripts/createChannel.sh create mode 100755 test-network/scripts/deployCC.sh create mode 100755 test-network/scripts/envVar.sh create mode 100755 test-network/scripts/org3-scripts/joinChannel.sh create mode 100755 test-network/scripts/org3-scripts/updateChannelConfig.sh create mode 100755 test-network/scripts/setAnchorPeer.sh create mode 100755 test-network/scripts/utils.sh create mode 100644 token-erc-20/README.md create mode 100644 token-erc-20/chaincode-go/chaincode/token_contract.go create mode 100644 token-erc-20/chaincode-go/go.mod create mode 100644 token-erc-20/chaincode-go/go.sum create mode 100644 token-erc-20/chaincode-go/token_erc_20.go create mode 100755 token-erc-20/chaincode-javascript/.editorconfig create mode 100644 token-erc-20/chaincode-javascript/.eslintignore create mode 100644 token-erc-20/chaincode-javascript/.eslintrc.js create mode 100644 token-erc-20/chaincode-javascript/.gitignore create mode 100644 token-erc-20/chaincode-javascript/index.js create mode 100644 token-erc-20/chaincode-javascript/lib/tokenERC20.js create mode 100644 token-erc-20/chaincode-javascript/package.json create mode 100644 token-erc-20/chaincode-javascript/test/tokenERC20.test.js 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/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..85a3d0d --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,14 @@ +repository: + name: fabric-samples + description: null + homepage: https://wiki.hyperledger.org/display/fabric + default_branch: master + has_downloads: true + has_issues: false + has_projects: false + has_wiki: false + archived: false + private: false + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4f9fd4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Emacs backup files +*~ +*# +.#* +# Vim file artifacts +.*.sw* +# installed platform-specific binaries +/bin +/config +.DS_Store +.project +# omit Go vendor directories +vendor/ +.vscode +.gradle +.idea +# Dependency directories +node_modules/ +# Ignore Gradle build output directory +build +package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2f6bf03 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2660 @@ +## "v2.1.0", "v2.0.1", "v2.0.0" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link + +## "v1.4.6" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link + +## "v1.4.5" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link + +## "v1.4.4" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link + +## "v1.4.3" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link + +## "v1.4.2" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link + +## "v1.4.1" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link + +## "v1.4.0" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [8a458b5](https://github.com/hyperledger/fabric-samples/commit/8a458b5) [FAB-12056](https://jira.hyperledger.org/browse/FAB-12056) Private marbles cc use transient data +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link +* [461b6ab](https://github.com/hyperledger/fabric-samples/commit/461b6ab) FABC-781 Remove fabric-ca sample +* [e9b9477](https://github.com/hyperledger/fabric-samples/commit/e9b9477) [FAB-13372](https://jira.hyperledger.org/browse/FAB-13372) Fabric-Samples return error msg +* [e3da220](https://github.com/hyperledger/fabric-samples/commit/e3da220) [FAB-13433](https://jira.hyperledger.org/browse/FAB-13433) - Update Jenkinsfile configuration +* [33db64e](https://github.com/hyperledger/fabric-samples/commit/33db64e) Configure Stale ProBot +* [5cd277f](https://github.com/hyperledger/fabric-samples/commit/5cd277f) [FAB-11951](https://jira.hyperledger.org/browse/FAB-11951) Interest-rate swap example for SBE +* [9567985](https://github.com/hyperledger/fabric-samples/commit/9567985) [FAB-13407](https://jira.hyperledger.org/browse/FAB-13407) Align fabric-samples with 1.4.0-rc2 release +* [c7572aa](https://github.com/hyperledger/fabric-samples/commit/c7572aa) [FAB-13305](https://jira.hyperledger.org/browse/FAB-13305) Update scripts to pull latest artifacts +* [ab46e35](https://github.com/hyperledger/fabric-samples/commit/ab46e35) [FAB-13283](https://jira.hyperledger.org/browse/FAB-13283) Update sample code for commercial paper +* [f677821](https://github.com/hyperledger/fabric-samples/commit/f677821) [FAB-13232](https://jira.hyperledger.org/browse/FAB-13232) fix peer node start command +* [6a7472e](https://github.com/hyperledger/fabric-samples/commit/6a7472e) [FAB-13126](https://jira.hyperledger.org/browse/FAB-13126) Align fabric-samples with 1.4.0-rc1 release +* [445ccbc](https://github.com/hyperledger/fabric-samples/commit/445ccbc) [FAB-12880](https://jira.hyperledger.org/browse/FAB-12880) Move old prog model samples for FabCar +* [5be62b5](https://github.com/hyperledger/fabric-samples/commit/5be62b5) [FAB-13207](https://jira.hyperledger.org/browse/FAB-13207) Remove incorrect discovery options +* [fdbd92d](https://github.com/hyperledger/fabric-samples/commit/fdbd92d) [FAB-13206](https://jira.hyperledger.org/browse/FAB-13206) Remove dependencies on fabric-client +* [eff0046](https://github.com/hyperledger/fabric-samples/commit/eff0046) [FAB-12877](https://jira.hyperledger.org/browse/FAB-12877) Add fabcar app using new prog model (JS) +* [c184196](https://github.com/hyperledger/fabric-samples/commit/c184196) [FAB-12878](https://jira.hyperledger.org/browse/FAB-12878) Add fabcar app using new prog model (TS) +* [e1a39e6](https://github.com/hyperledger/fabric-samples/commit/e1a39e6) [FAB-12724](https://jira.hyperledger.org/browse/FAB-12724) Upgrade from 1.3.x to 1.4.0 +* [c21bbba](https://github.com/hyperledger/fabric-samples/commit/c21bbba) Update samples to use new logging env variables +* [7ad9f19](https://github.com/hyperledger/fabric-samples/commit/7ad9f19) [FAB-13011](https://jira.hyperledger.org/browse/FAB-13011) add kafka consensus type to byfn sample +* [33f064f](https://github.com/hyperledger/fabric-samples/commit/33f064f) [FAB-13170](https://jira.hyperledger.org/browse/FAB-13170) Add memberOnlyRead to marbles sample +* [928b72b](https://github.com/hyperledger/fabric-samples/commit/928b72b) [FAB-12875](https://jira.hyperledger.org/browse/FAB-12875) Add automated tests for fabcar sample +* [5c087f1](https://github.com/hyperledger/fabric-samples/commit/5c087f1) [FAB-13046](https://jira.hyperledger.org/browse/FAB-13046) Update TypeScript contract dependencies +* [3748983](https://github.com/hyperledger/fabric-samples/commit/3748983) [FAB-12879](https://jira.hyperledger.org/browse/FAB-12879) Update fabcar script for new contracts +* [4fb3b57](https://github.com/hyperledger/fabric-samples/commit/4fb3b57) [FAB-12852](https://jira.hyperledger.org/browse/FAB-12852) Add fabcar contract w/ new prog model (TS) +* [9facb42](https://github.com/hyperledger/fabric-samples/commit/9facb42) [FAB-12851](https://jira.hyperledger.org/browse/FAB-12851) Add fabcar contract w/ new prog model (JS) +* [e67fcf1](https://github.com/hyperledger/fabric-samples/commit/e67fcf1) [FAB-12322](https://jira.hyperledger.org/browse/FAB-12322) Update commercial-paper sample +* [fd6e2c4](https://github.com/hyperledger/fabric-samples/commit/fd6e2c4) [FAB-12703](https://jira.hyperledger.org/browse/FAB-12703) Fix misspelling "lauches" +* [c05f172](https://github.com/hyperledger/fabric-samples/commit/c05f172) [FAB-12608](https://jira.hyperledger.org/browse/FAB-12608) Update pipeline script +* [286861e](https://github.com/hyperledger/fabric-samples/commit/286861e) [FAB-12371](https://jira.hyperledger.org/browse/FAB-12371)Fix the abac sample to use new cid package +* [24c5e47](https://github.com/hyperledger/fabric-samples/commit/24c5e47) [FAB-12026](https://jira.hyperledger.org/browse/FAB-12026) pagination samples for node marbles02 +* [6dc5ce5](https://github.com/hyperledger/fabric-samples/commit/6dc5ce5) [FAB-12587](https://jira.hyperledger.org/browse/FAB-12587) Fix for Query Block by block hash API +* [df311ce](https://github.com/hyperledger/fabric-samples/commit/df311ce) [FAB-12173](https://jira.hyperledger.org/browse/FAB-12173) balance-transfer: Update anchor peers +* [c925148](https://github.com/hyperledger/fabric-samples/commit/c925148) [FAB-12415](https://jira.hyperledger.org/browse/FAB-12415) samples for 1.3.0 (master cleanup) +* [3a12c60](https://github.com/hyperledger/fabric-samples/commit/3a12c60) [FAB-12275](https://jira.hyperledger.org/browse/FAB-12275) Fix the warn in creating genesis block +* [c6f6324](https://github.com/hyperledger/fabric-samples/commit/c6f6324) [FAB-12257](https://jira.hyperledger.org/browse/FAB-12257) allow balance-transfer for doscovery +* [4445e8d](https://github.com/hyperledger/fabric-samples/commit/4445e8d) [FAB-12272](https://jira.hyperledger.org/browse/FAB-12272) Increase MAX_RETRY to 10 +* [33d333f](https://github.com/hyperledger/fabric-samples/commit/33d333f) [FAB-12190](https://jira.hyperledger.org/browse/FAB-12190) Update stable version in CI scripts +* [4089786](https://github.com/hyperledger/fabric-samples/commit/4089786) [FAB-11867](https://jira.hyperledger.org/browse/FAB-11867) Develop Apps:Sample pt 2 -- application +* [d776651](https://github.com/hyperledger/fabric-samples/commit/d776651) [FAB-11723](https://jira.hyperledger.org/browse/FAB-11723) Developing Apps: Sample pt 1 -- contract + +## "v1.3.0" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [8a458b5](https://github.com/hyperledger/fabric-samples/commit/8a458b5) [FAB-12056](https://jira.hyperledger.org/browse/FAB-12056) Private marbles cc use transient data +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link +* [461b6ab](https://github.com/hyperledger/fabric-samples/commit/461b6ab) FABC-781 Remove fabric-ca sample +* [e9b9477](https://github.com/hyperledger/fabric-samples/commit/e9b9477) [FAB-13372](https://jira.hyperledger.org/browse/FAB-13372) Fabric-Samples return error msg +* [e3da220](https://github.com/hyperledger/fabric-samples/commit/e3da220) [FAB-13433](https://jira.hyperledger.org/browse/FAB-13433) - Update Jenkinsfile configuration +* [33db64e](https://github.com/hyperledger/fabric-samples/commit/33db64e) Configure Stale ProBot +* [5cd277f](https://github.com/hyperledger/fabric-samples/commit/5cd277f) [FAB-11951](https://jira.hyperledger.org/browse/FAB-11951) Interest-rate swap example for SBE +* [9567985](https://github.com/hyperledger/fabric-samples/commit/9567985) [FAB-13407](https://jira.hyperledger.org/browse/FAB-13407) Align fabric-samples with 1.4.0-rc2 release +* [c7572aa](https://github.com/hyperledger/fabric-samples/commit/c7572aa) [FAB-13305](https://jira.hyperledger.org/browse/FAB-13305) Update scripts to pull latest artifacts +* [ab46e35](https://github.com/hyperledger/fabric-samples/commit/ab46e35) [FAB-13283](https://jira.hyperledger.org/browse/FAB-13283) Update sample code for commercial paper +* [f677821](https://github.com/hyperledger/fabric-samples/commit/f677821) [FAB-13232](https://jira.hyperledger.org/browse/FAB-13232) fix peer node start command +* [6a7472e](https://github.com/hyperledger/fabric-samples/commit/6a7472e) [FAB-13126](https://jira.hyperledger.org/browse/FAB-13126) Align fabric-samples with 1.4.0-rc1 release +* [445ccbc](https://github.com/hyperledger/fabric-samples/commit/445ccbc) [FAB-12880](https://jira.hyperledger.org/browse/FAB-12880) Move old prog model samples for FabCar +* [5be62b5](https://github.com/hyperledger/fabric-samples/commit/5be62b5) [FAB-13207](https://jira.hyperledger.org/browse/FAB-13207) Remove incorrect discovery options +* [fdbd92d](https://github.com/hyperledger/fabric-samples/commit/fdbd92d) [FAB-13206](https://jira.hyperledger.org/browse/FAB-13206) Remove dependencies on fabric-client +* [eff0046](https://github.com/hyperledger/fabric-samples/commit/eff0046) [FAB-12877](https://jira.hyperledger.org/browse/FAB-12877) Add fabcar app using new prog model (JS) +* [c184196](https://github.com/hyperledger/fabric-samples/commit/c184196) [FAB-12878](https://jira.hyperledger.org/browse/FAB-12878) Add fabcar app using new prog model (TS) +* [e1a39e6](https://github.com/hyperledger/fabric-samples/commit/e1a39e6) [FAB-12724](https://jira.hyperledger.org/browse/FAB-12724) Upgrade from 1.3.x to 1.4.0 +* [c21bbba](https://github.com/hyperledger/fabric-samples/commit/c21bbba) Update samples to use new logging env variables +* [7ad9f19](https://github.com/hyperledger/fabric-samples/commit/7ad9f19) [FAB-13011](https://jira.hyperledger.org/browse/FAB-13011) add kafka consensus type to byfn sample +* [33f064f](https://github.com/hyperledger/fabric-samples/commit/33f064f) [FAB-13170](https://jira.hyperledger.org/browse/FAB-13170) Add memberOnlyRead to marbles sample +* [928b72b](https://github.com/hyperledger/fabric-samples/commit/928b72b) [FAB-12875](https://jira.hyperledger.org/browse/FAB-12875) Add automated tests for fabcar sample +* [5c087f1](https://github.com/hyperledger/fabric-samples/commit/5c087f1) [FAB-13046](https://jira.hyperledger.org/browse/FAB-13046) Update TypeScript contract dependencies +* [3748983](https://github.com/hyperledger/fabric-samples/commit/3748983) [FAB-12879](https://jira.hyperledger.org/browse/FAB-12879) Update fabcar script for new contracts +* [4fb3b57](https://github.com/hyperledger/fabric-samples/commit/4fb3b57) [FAB-12852](https://jira.hyperledger.org/browse/FAB-12852) Add fabcar contract w/ new prog model (TS) +* [9facb42](https://github.com/hyperledger/fabric-samples/commit/9facb42) [FAB-12851](https://jira.hyperledger.org/browse/FAB-12851) Add fabcar contract w/ new prog model (JS) +* [e67fcf1](https://github.com/hyperledger/fabric-samples/commit/e67fcf1) [FAB-12322](https://jira.hyperledger.org/browse/FAB-12322) Update commercial-paper sample +* [fd6e2c4](https://github.com/hyperledger/fabric-samples/commit/fd6e2c4) [FAB-12703](https://jira.hyperledger.org/browse/FAB-12703) Fix misspelling "lauches" +* [c05f172](https://github.com/hyperledger/fabric-samples/commit/c05f172) [FAB-12608](https://jira.hyperledger.org/browse/FAB-12608) Update pipeline script +* [286861e](https://github.com/hyperledger/fabric-samples/commit/286861e) [FAB-12371](https://jira.hyperledger.org/browse/FAB-12371)Fix the abac sample to use new cid package +* [24c5e47](https://github.com/hyperledger/fabric-samples/commit/24c5e47) [FAB-12026](https://jira.hyperledger.org/browse/FAB-12026) pagination samples for node marbles02 +* [6dc5ce5](https://github.com/hyperledger/fabric-samples/commit/6dc5ce5) [FAB-12587](https://jira.hyperledger.org/browse/FAB-12587) Fix for Query Block by block hash API +* [df311ce](https://github.com/hyperledger/fabric-samples/commit/df311ce) [FAB-12173](https://jira.hyperledger.org/browse/FAB-12173) balance-transfer: Update anchor peers +* [c925148](https://github.com/hyperledger/fabric-samples/commit/c925148) [FAB-12415](https://jira.hyperledger.org/browse/FAB-12415) samples for 1.3.0 (master cleanup) +* [3a12c60](https://github.com/hyperledger/fabric-samples/commit/3a12c60) [FAB-12275](https://jira.hyperledger.org/browse/FAB-12275) Fix the warn in creating genesis block +* [c6f6324](https://github.com/hyperledger/fabric-samples/commit/c6f6324) [FAB-12257](https://jira.hyperledger.org/browse/FAB-12257) allow balance-transfer for doscovery +* [4445e8d](https://github.com/hyperledger/fabric-samples/commit/4445e8d) [FAB-12272](https://jira.hyperledger.org/browse/FAB-12272) Increase MAX_RETRY to 10 +* [33d333f](https://github.com/hyperledger/fabric-samples/commit/33d333f) [FAB-12190](https://jira.hyperledger.org/browse/FAB-12190) Update stable version in CI scripts +* [edee638](https://github.com/hyperledger/fabric-samples/commit/edee638) [FAB-12184](https://jira.hyperledger.org/browse/FAB-12184) Prepare fabric-samples for 1.3.0-rc1 +* [514d456](https://github.com/hyperledger/fabric-samples/commit/514d456) [FAB-12170](https://jira.hyperledger.org/browse/FAB-12170) Fix dependency check in java chaincode +* [9f80e47](https://github.com/hyperledger/fabric-samples/commit/9f80e47) [FAB-12119](https://jira.hyperledger.org/browse/FAB-12119) Fix groupId in java chaincodes +* [3237229](https://github.com/hyperledger/fabric-samples/commit/3237229) [FAB-12073](https://jira.hyperledger.org/browse/FAB-12073) Fix Org3 peers CouchDB config. +* [eece3d8](https://github.com/hyperledger/fabric-samples/commit/eece3d8) [FAB-12106](https://jira.hyperledger.org/browse/FAB-12106) Update fabric-ca build scripts +* [f62952f](https://github.com/hyperledger/fabric-samples/commit/f62952f) [FABC-131] Change fabric-ca sample to build images +* [4089786](https://github.com/hyperledger/fabric-samples/commit/4089786) [FAB-11867](https://jira.hyperledger.org/browse/FAB-11867) Develop Apps:Sample pt 2 -- application +* [9ee57c6](https://github.com/hyperledger/fabric-samples/commit/9ee57c6) [FAB-11778](https://jira.hyperledger.org/browse/FAB-11778) Upgrade to v1.3.x from v1.2.x in byfn +* [e7a1b76](https://github.com/hyperledger/fabric-samples/commit/e7a1b76) [FAB-9386](https://jira.hyperledger.org/browse/FAB-9386) Remove Marbles sample reference to Fauxton +* [9c6acee](https://github.com/hyperledger/fabric-samples/commit/9c6acee) [FAB-12022](https://jira.hyperledger.org/browse/FAB-12022) Fix CI by increasing couchdb timeout +* [4030ebd](https://github.com/hyperledger/fabric-samples/commit/4030ebd) [FAB-11397](https://jira.hyperledger.org/browse/FAB-11397) Adding java cc +* [d776651](https://github.com/hyperledger/fabric-samples/commit/d776651) [FAB-11723](https://jira.hyperledger.org/browse/FAB-11723) Developing Apps: Sample pt 1 -- contract +* [cbbbc78](https://github.com/hyperledger/fabric-samples/commit/cbbbc78) [FAB-11577](https://jira.hyperledger.org/browse/FAB-11577) Fix balance transfer to install Chaincode +* [bfdc0b6](https://github.com/hyperledger/fabric-samples/commit/bfdc0b6) [FAB-11518](https://jira.hyperledger.org/browse/FAB-11518) +* [5930dfc](https://github.com/hyperledger/fabric-samples/commit/5930dfc) [FAB-11488](https://jira.hyperledger.org/browse/FAB-11488) Update CI script +* [ca6959c](https://github.com/hyperledger/fabric-samples/commit/ca6959c) [FAB-11311](https://jira.hyperledger.org/browse/FAB-11311) Update fabric image version +* [0ca9e6e](https://github.com/hyperledger/fabric-samples/commit/0ca9e6e) FABN-833 Update Jenkinsfile to capture build artifacts +* [a4a15cb](https://github.com/hyperledger/fabric-samples/commit/a4a15cb) [FAB-11220](https://jira.hyperledger.org/browse/FAB-11220) Samples - remove EventHub +* [c4bdc68](https://github.com/hyperledger/fabric-samples/commit/c4bdc68) [FAB-8479](https://jira.hyperledger.org/browse/FAB-8479) Added Endorsement policy +* [6edd320](https://github.com/hyperledger/fabric-samples/commit/6edd320) [FAB-9297](https://jira.hyperledger.org/browse/FAB-9297) fix README links and update bootstrap.sh +* [75e2931](https://github.com/hyperledger/fabric-samples/commit/75e2931) [FAB-10811](https://jira.hyperledger.org/browse/FAB-10811) fabric-ca sample is broken on v1.2 + +## "v1.2.1" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [8a458b5](https://github.com/hyperledger/fabric-samples/commit/8a458b5) [FAB-12056](https://jira.hyperledger.org/browse/FAB-12056) Private marbles cc use transient data +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link +* [461b6ab](https://github.com/hyperledger/fabric-samples/commit/461b6ab) FABC-781 Remove fabric-ca sample +* [e9b9477](https://github.com/hyperledger/fabric-samples/commit/e9b9477) [FAB-13372](https://jira.hyperledger.org/browse/FAB-13372) Fabric-Samples return error msg +* [e3da220](https://github.com/hyperledger/fabric-samples/commit/e3da220) [FAB-13433](https://jira.hyperledger.org/browse/FAB-13433) - Update Jenkinsfile configuration +* [33db64e](https://github.com/hyperledger/fabric-samples/commit/33db64e) Configure Stale ProBot +* [5cd277f](https://github.com/hyperledger/fabric-samples/commit/5cd277f) [FAB-11951](https://jira.hyperledger.org/browse/FAB-11951) Interest-rate swap example for SBE +* [9567985](https://github.com/hyperledger/fabric-samples/commit/9567985) [FAB-13407](https://jira.hyperledger.org/browse/FAB-13407) Align fabric-samples with 1.4.0-rc2 release +* [c7572aa](https://github.com/hyperledger/fabric-samples/commit/c7572aa) [FAB-13305](https://jira.hyperledger.org/browse/FAB-13305) Update scripts to pull latest artifacts +* [ab46e35](https://github.com/hyperledger/fabric-samples/commit/ab46e35) [FAB-13283](https://jira.hyperledger.org/browse/FAB-13283) Update sample code for commercial paper +* [f677821](https://github.com/hyperledger/fabric-samples/commit/f677821) [FAB-13232](https://jira.hyperledger.org/browse/FAB-13232) fix peer node start command +* [6a7472e](https://github.com/hyperledger/fabric-samples/commit/6a7472e) [FAB-13126](https://jira.hyperledger.org/browse/FAB-13126) Align fabric-samples with 1.4.0-rc1 release +* [445ccbc](https://github.com/hyperledger/fabric-samples/commit/445ccbc) [FAB-12880](https://jira.hyperledger.org/browse/FAB-12880) Move old prog model samples for FabCar +* [5be62b5](https://github.com/hyperledger/fabric-samples/commit/5be62b5) [FAB-13207](https://jira.hyperledger.org/browse/FAB-13207) Remove incorrect discovery options +* [fdbd92d](https://github.com/hyperledger/fabric-samples/commit/fdbd92d) [FAB-13206](https://jira.hyperledger.org/browse/FAB-13206) Remove dependencies on fabric-client +* [eff0046](https://github.com/hyperledger/fabric-samples/commit/eff0046) [FAB-12877](https://jira.hyperledger.org/browse/FAB-12877) Add fabcar app using new prog model (JS) +* [c184196](https://github.com/hyperledger/fabric-samples/commit/c184196) [FAB-12878](https://jira.hyperledger.org/browse/FAB-12878) Add fabcar app using new prog model (TS) +* [e1a39e6](https://github.com/hyperledger/fabric-samples/commit/e1a39e6) [FAB-12724](https://jira.hyperledger.org/browse/FAB-12724) Upgrade from 1.3.x to 1.4.0 +* [c21bbba](https://github.com/hyperledger/fabric-samples/commit/c21bbba) Update samples to use new logging env variables +* [7ad9f19](https://github.com/hyperledger/fabric-samples/commit/7ad9f19) [FAB-13011](https://jira.hyperledger.org/browse/FAB-13011) add kafka consensus type to byfn sample +* [33f064f](https://github.com/hyperledger/fabric-samples/commit/33f064f) [FAB-13170](https://jira.hyperledger.org/browse/FAB-13170) Add memberOnlyRead to marbles sample +* [928b72b](https://github.com/hyperledger/fabric-samples/commit/928b72b) [FAB-12875](https://jira.hyperledger.org/browse/FAB-12875) Add automated tests for fabcar sample +* [5c087f1](https://github.com/hyperledger/fabric-samples/commit/5c087f1) [FAB-13046](https://jira.hyperledger.org/browse/FAB-13046) Update TypeScript contract dependencies +* [3748983](https://github.com/hyperledger/fabric-samples/commit/3748983) [FAB-12879](https://jira.hyperledger.org/browse/FAB-12879) Update fabcar script for new contracts +* [4fb3b57](https://github.com/hyperledger/fabric-samples/commit/4fb3b57) [FAB-12852](https://jira.hyperledger.org/browse/FAB-12852) Add fabcar contract w/ new prog model (TS) +* [9facb42](https://github.com/hyperledger/fabric-samples/commit/9facb42) [FAB-12851](https://jira.hyperledger.org/browse/FAB-12851) Add fabcar contract w/ new prog model (JS) +* [e67fcf1](https://github.com/hyperledger/fabric-samples/commit/e67fcf1) [FAB-12322](https://jira.hyperledger.org/browse/FAB-12322) Update commercial-paper sample +* [fd6e2c4](https://github.com/hyperledger/fabric-samples/commit/fd6e2c4) [FAB-12703](https://jira.hyperledger.org/browse/FAB-12703) Fix misspelling "lauches" +* [c05f172](https://github.com/hyperledger/fabric-samples/commit/c05f172) [FAB-12608](https://jira.hyperledger.org/browse/FAB-12608) Update pipeline script +* [286861e](https://github.com/hyperledger/fabric-samples/commit/286861e) [FAB-12371](https://jira.hyperledger.org/browse/FAB-12371)Fix the abac sample to use new cid package +* [24c5e47](https://github.com/hyperledger/fabric-samples/commit/24c5e47) [FAB-12026](https://jira.hyperledger.org/browse/FAB-12026) pagination samples for node marbles02 +* [6dc5ce5](https://github.com/hyperledger/fabric-samples/commit/6dc5ce5) [FAB-12587](https://jira.hyperledger.org/browse/FAB-12587) Fix for Query Block by block hash API +* [df311ce](https://github.com/hyperledger/fabric-samples/commit/df311ce) [FAB-12173](https://jira.hyperledger.org/browse/FAB-12173) balance-transfer: Update anchor peers +* [c925148](https://github.com/hyperledger/fabric-samples/commit/c925148) [FAB-12415](https://jira.hyperledger.org/browse/FAB-12415) samples for 1.3.0 (master cleanup) +* [3a12c60](https://github.com/hyperledger/fabric-samples/commit/3a12c60) [FAB-12275](https://jira.hyperledger.org/browse/FAB-12275) Fix the warn in creating genesis block +* [c6f6324](https://github.com/hyperledger/fabric-samples/commit/c6f6324) [FAB-12257](https://jira.hyperledger.org/browse/FAB-12257) allow balance-transfer for doscovery +* [4445e8d](https://github.com/hyperledger/fabric-samples/commit/4445e8d) [FAB-12272](https://jira.hyperledger.org/browse/FAB-12272) Increase MAX_RETRY to 10 +* [33d333f](https://github.com/hyperledger/fabric-samples/commit/33d333f) [FAB-12190](https://jira.hyperledger.org/browse/FAB-12190) Update stable version in CI scripts +* [edee638](https://github.com/hyperledger/fabric-samples/commit/edee638) [FAB-12184](https://jira.hyperledger.org/browse/FAB-12184) Prepare fabric-samples for 1.3.0-rc1 +* [514d456](https://github.com/hyperledger/fabric-samples/commit/514d456) [FAB-12170](https://jira.hyperledger.org/browse/FAB-12170) Fix dependency check in java chaincode +* [9f80e47](https://github.com/hyperledger/fabric-samples/commit/9f80e47) [FAB-12119](https://jira.hyperledger.org/browse/FAB-12119) Fix groupId in java chaincodes +* [3237229](https://github.com/hyperledger/fabric-samples/commit/3237229) [FAB-12073](https://jira.hyperledger.org/browse/FAB-12073) Fix Org3 peers CouchDB config. +* [eece3d8](https://github.com/hyperledger/fabric-samples/commit/eece3d8) [FAB-12106](https://jira.hyperledger.org/browse/FAB-12106) Update fabric-ca build scripts +* [f62952f](https://github.com/hyperledger/fabric-samples/commit/f62952f) [FABC-131] Change fabric-ca sample to build images +* [4089786](https://github.com/hyperledger/fabric-samples/commit/4089786) [FAB-11867](https://jira.hyperledger.org/browse/FAB-11867) Develop Apps:Sample pt 2 -- application +* [9ee57c6](https://github.com/hyperledger/fabric-samples/commit/9ee57c6) [FAB-11778](https://jira.hyperledger.org/browse/FAB-11778) Upgrade to v1.3.x from v1.2.x in byfn +* [e7a1b76](https://github.com/hyperledger/fabric-samples/commit/e7a1b76) [FAB-9386](https://jira.hyperledger.org/browse/FAB-9386) Remove Marbles sample reference to Fauxton +* [9c6acee](https://github.com/hyperledger/fabric-samples/commit/9c6acee) [FAB-12022](https://jira.hyperledger.org/browse/FAB-12022) Fix CI by increasing couchdb timeout +* [4030ebd](https://github.com/hyperledger/fabric-samples/commit/4030ebd) [FAB-11397](https://jira.hyperledger.org/browse/FAB-11397) Adding java cc +* [d776651](https://github.com/hyperledger/fabric-samples/commit/d776651) [FAB-11723](https://jira.hyperledger.org/browse/FAB-11723) Developing Apps: Sample pt 1 -- contract +* [cbbbc78](https://github.com/hyperledger/fabric-samples/commit/cbbbc78) [FAB-11577](https://jira.hyperledger.org/browse/FAB-11577) Fix balance transfer to install Chaincode +* [bfdc0b6](https://github.com/hyperledger/fabric-samples/commit/bfdc0b6) [FAB-11518](https://jira.hyperledger.org/browse/FAB-11518) +* [5930dfc](https://github.com/hyperledger/fabric-samples/commit/5930dfc) [FAB-11488](https://jira.hyperledger.org/browse/FAB-11488) Update CI script +* [ca6959c](https://github.com/hyperledger/fabric-samples/commit/ca6959c) [FAB-11311](https://jira.hyperledger.org/browse/FAB-11311) Update fabric image version +* [0ca9e6e](https://github.com/hyperledger/fabric-samples/commit/0ca9e6e) FABN-833 Update Jenkinsfile to capture build artifacts +* [a4a15cb](https://github.com/hyperledger/fabric-samples/commit/a4a15cb) [FAB-11220](https://jira.hyperledger.org/browse/FAB-11220) Samples - remove EventHub +* [c4bdc68](https://github.com/hyperledger/fabric-samples/commit/c4bdc68) [FAB-8479](https://jira.hyperledger.org/browse/FAB-8479) Added Endorsement policy +* [6edd320](https://github.com/hyperledger/fabric-samples/commit/6edd320) [FAB-9297](https://jira.hyperledger.org/browse/FAB-9297) fix README links and update bootstrap.sh +* [75e2931](https://github.com/hyperledger/fabric-samples/commit/75e2931) [FAB-10811](https://jira.hyperledger.org/browse/FAB-10811) fabric-ca sample is broken on v1.2 + +## "v1.2.0" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [8a458b5](https://github.com/hyperledger/fabric-samples/commit/8a458b5) [FAB-12056](https://jira.hyperledger.org/browse/FAB-12056) Private marbles cc use transient data +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link +* [461b6ab](https://github.com/hyperledger/fabric-samples/commit/461b6ab) FABC-781 Remove fabric-ca sample +* [e9b9477](https://github.com/hyperledger/fabric-samples/commit/e9b9477) [FAB-13372](https://jira.hyperledger.org/browse/FAB-13372) Fabric-Samples return error msg +* [e3da220](https://github.com/hyperledger/fabric-samples/commit/e3da220) [FAB-13433](https://jira.hyperledger.org/browse/FAB-13433) - Update Jenkinsfile configuration +* [33db64e](https://github.com/hyperledger/fabric-samples/commit/33db64e) Configure Stale ProBot +* [5cd277f](https://github.com/hyperledger/fabric-samples/commit/5cd277f) [FAB-11951](https://jira.hyperledger.org/browse/FAB-11951) Interest-rate swap example for SBE +* [9567985](https://github.com/hyperledger/fabric-samples/commit/9567985) [FAB-13407](https://jira.hyperledger.org/browse/FAB-13407) Align fabric-samples with 1.4.0-rc2 release +* [c7572aa](https://github.com/hyperledger/fabric-samples/commit/c7572aa) [FAB-13305](https://jira.hyperledger.org/browse/FAB-13305) Update scripts to pull latest artifacts +* [ab46e35](https://github.com/hyperledger/fabric-samples/commit/ab46e35) [FAB-13283](https://jira.hyperledger.org/browse/FAB-13283) Update sample code for commercial paper +* [f677821](https://github.com/hyperledger/fabric-samples/commit/f677821) [FAB-13232](https://jira.hyperledger.org/browse/FAB-13232) fix peer node start command +* [6a7472e](https://github.com/hyperledger/fabric-samples/commit/6a7472e) [FAB-13126](https://jira.hyperledger.org/browse/FAB-13126) Align fabric-samples with 1.4.0-rc1 release +* [445ccbc](https://github.com/hyperledger/fabric-samples/commit/445ccbc) [FAB-12880](https://jira.hyperledger.org/browse/FAB-12880) Move old prog model samples for FabCar +* [5be62b5](https://github.com/hyperledger/fabric-samples/commit/5be62b5) [FAB-13207](https://jira.hyperledger.org/browse/FAB-13207) Remove incorrect discovery options +* [fdbd92d](https://github.com/hyperledger/fabric-samples/commit/fdbd92d) [FAB-13206](https://jira.hyperledger.org/browse/FAB-13206) Remove dependencies on fabric-client +* [eff0046](https://github.com/hyperledger/fabric-samples/commit/eff0046) [FAB-12877](https://jira.hyperledger.org/browse/FAB-12877) Add fabcar app using new prog model (JS) +* [c184196](https://github.com/hyperledger/fabric-samples/commit/c184196) [FAB-12878](https://jira.hyperledger.org/browse/FAB-12878) Add fabcar app using new prog model (TS) +* [e1a39e6](https://github.com/hyperledger/fabric-samples/commit/e1a39e6) [FAB-12724](https://jira.hyperledger.org/browse/FAB-12724) Upgrade from 1.3.x to 1.4.0 +* [c21bbba](https://github.com/hyperledger/fabric-samples/commit/c21bbba) Update samples to use new logging env variables +* [7ad9f19](https://github.com/hyperledger/fabric-samples/commit/7ad9f19) [FAB-13011](https://jira.hyperledger.org/browse/FAB-13011) add kafka consensus type to byfn sample +* [33f064f](https://github.com/hyperledger/fabric-samples/commit/33f064f) [FAB-13170](https://jira.hyperledger.org/browse/FAB-13170) Add memberOnlyRead to marbles sample +* [928b72b](https://github.com/hyperledger/fabric-samples/commit/928b72b) [FAB-12875](https://jira.hyperledger.org/browse/FAB-12875) Add automated tests for fabcar sample +* [5c087f1](https://github.com/hyperledger/fabric-samples/commit/5c087f1) [FAB-13046](https://jira.hyperledger.org/browse/FAB-13046) Update TypeScript contract dependencies +* [3748983](https://github.com/hyperledger/fabric-samples/commit/3748983) [FAB-12879](https://jira.hyperledger.org/browse/FAB-12879) Update fabcar script for new contracts +* [4fb3b57](https://github.com/hyperledger/fabric-samples/commit/4fb3b57) [FAB-12852](https://jira.hyperledger.org/browse/FAB-12852) Add fabcar contract w/ new prog model (TS) +* [9facb42](https://github.com/hyperledger/fabric-samples/commit/9facb42) [FAB-12851](https://jira.hyperledger.org/browse/FAB-12851) Add fabcar contract w/ new prog model (JS) +* [e67fcf1](https://github.com/hyperledger/fabric-samples/commit/e67fcf1) [FAB-12322](https://jira.hyperledger.org/browse/FAB-12322) Update commercial-paper sample +* [fd6e2c4](https://github.com/hyperledger/fabric-samples/commit/fd6e2c4) [FAB-12703](https://jira.hyperledger.org/browse/FAB-12703) Fix misspelling "lauches" +* [c05f172](https://github.com/hyperledger/fabric-samples/commit/c05f172) [FAB-12608](https://jira.hyperledger.org/browse/FAB-12608) Update pipeline script +* [286861e](https://github.com/hyperledger/fabric-samples/commit/286861e) [FAB-12371](https://jira.hyperledger.org/browse/FAB-12371)Fix the abac sample to use new cid package +* [24c5e47](https://github.com/hyperledger/fabric-samples/commit/24c5e47) [FAB-12026](https://jira.hyperledger.org/browse/FAB-12026) pagination samples for node marbles02 +* [6dc5ce5](https://github.com/hyperledger/fabric-samples/commit/6dc5ce5) [FAB-12587](https://jira.hyperledger.org/browse/FAB-12587) Fix for Query Block by block hash API +* [df311ce](https://github.com/hyperledger/fabric-samples/commit/df311ce) [FAB-12173](https://jira.hyperledger.org/browse/FAB-12173) balance-transfer: Update anchor peers +* [c925148](https://github.com/hyperledger/fabric-samples/commit/c925148) [FAB-12415](https://jira.hyperledger.org/browse/FAB-12415) samples for 1.3.0 (master cleanup) +* [3a12c60](https://github.com/hyperledger/fabric-samples/commit/3a12c60) [FAB-12275](https://jira.hyperledger.org/browse/FAB-12275) Fix the warn in creating genesis block +* [c6f6324](https://github.com/hyperledger/fabric-samples/commit/c6f6324) [FAB-12257](https://jira.hyperledger.org/browse/FAB-12257) allow balance-transfer for doscovery +* [4445e8d](https://github.com/hyperledger/fabric-samples/commit/4445e8d) [FAB-12272](https://jira.hyperledger.org/browse/FAB-12272) Increase MAX_RETRY to 10 +* [33d333f](https://github.com/hyperledger/fabric-samples/commit/33d333f) [FAB-12190](https://jira.hyperledger.org/browse/FAB-12190) Update stable version in CI scripts +* [edee638](https://github.com/hyperledger/fabric-samples/commit/edee638) [FAB-12184](https://jira.hyperledger.org/browse/FAB-12184) Prepare fabric-samples for 1.3.0-rc1 +* [514d456](https://github.com/hyperledger/fabric-samples/commit/514d456) [FAB-12170](https://jira.hyperledger.org/browse/FAB-12170) Fix dependency check in java chaincode +* [9f80e47](https://github.com/hyperledger/fabric-samples/commit/9f80e47) [FAB-12119](https://jira.hyperledger.org/browse/FAB-12119) Fix groupId in java chaincodes +* [3237229](https://github.com/hyperledger/fabric-samples/commit/3237229) [FAB-12073](https://jira.hyperledger.org/browse/FAB-12073) Fix Org3 peers CouchDB config. +* [eece3d8](https://github.com/hyperledger/fabric-samples/commit/eece3d8) [FAB-12106](https://jira.hyperledger.org/browse/FAB-12106) Update fabric-ca build scripts +* [f62952f](https://github.com/hyperledger/fabric-samples/commit/f62952f) [FABC-131] Change fabric-ca sample to build images +* [4089786](https://github.com/hyperledger/fabric-samples/commit/4089786) [FAB-11867](https://jira.hyperledger.org/browse/FAB-11867) Develop Apps:Sample pt 2 -- application +* [9ee57c6](https://github.com/hyperledger/fabric-samples/commit/9ee57c6) [FAB-11778](https://jira.hyperledger.org/browse/FAB-11778) Upgrade to v1.3.x from v1.2.x in byfn +* [e7a1b76](https://github.com/hyperledger/fabric-samples/commit/e7a1b76) [FAB-9386](https://jira.hyperledger.org/browse/FAB-9386) Remove Marbles sample reference to Fauxton +* [9c6acee](https://github.com/hyperledger/fabric-samples/commit/9c6acee) [FAB-12022](https://jira.hyperledger.org/browse/FAB-12022) Fix CI by increasing couchdb timeout +* [4030ebd](https://github.com/hyperledger/fabric-samples/commit/4030ebd) [FAB-11397](https://jira.hyperledger.org/browse/FAB-11397) Adding java cc +* [d776651](https://github.com/hyperledger/fabric-samples/commit/d776651) [FAB-11723](https://jira.hyperledger.org/browse/FAB-11723) Developing Apps: Sample pt 1 -- contract +* [cbbbc78](https://github.com/hyperledger/fabric-samples/commit/cbbbc78) [FAB-11577](https://jira.hyperledger.org/browse/FAB-11577) Fix balance transfer to install Chaincode +* [bfdc0b6](https://github.com/hyperledger/fabric-samples/commit/bfdc0b6) [FAB-11518](https://jira.hyperledger.org/browse/FAB-11518) +* [5930dfc](https://github.com/hyperledger/fabric-samples/commit/5930dfc) [FAB-11488](https://jira.hyperledger.org/browse/FAB-11488) Update CI script +* [ca6959c](https://github.com/hyperledger/fabric-samples/commit/ca6959c) [FAB-11311](https://jira.hyperledger.org/browse/FAB-11311) Update fabric image version +* [0ca9e6e](https://github.com/hyperledger/fabric-samples/commit/0ca9e6e) FABN-833 Update Jenkinsfile to capture build artifacts +* [a4a15cb](https://github.com/hyperledger/fabric-samples/commit/a4a15cb) [FAB-11220](https://jira.hyperledger.org/browse/FAB-11220) Samples - remove EventHub +* [c4bdc68](https://github.com/hyperledger/fabric-samples/commit/c4bdc68) [FAB-8479](https://jira.hyperledger.org/browse/FAB-8479) Added Endorsement policy +* [6edd320](https://github.com/hyperledger/fabric-samples/commit/6edd320) [FAB-9297](https://jira.hyperledger.org/browse/FAB-9297) fix README links and update bootstrap.sh +* [75e2931](https://github.com/hyperledger/fabric-samples/commit/75e2931) [FAB-10811](https://jira.hyperledger.org/browse/FAB-10811) fabric-ca sample is broken on v1.2 +* [ad40e29](https://github.com/hyperledger/fabric-samples/commit/ad40e29) [FAB-10732](https://jira.hyperledger.org/browse/FAB-10732) BYFN upgrade to v1.2 +* [20ad472](https://github.com/hyperledger/fabric-samples/commit/20ad472) [FAB-10801](https://jira.hyperledger.org/browse/FAB-10801) format and styling for shell artifacts +* [e95210e](https://github.com/hyperledger/fabric-samples/commit/e95210e) [FAB-10074](https://jira.hyperledger.org/browse/FAB-10074) CI Script for pipeline project type +* [5956178](https://github.com/hyperledger/fabric-samples/commit/5956178) [FAB-6600](https://jira.hyperledger.org/browse/FAB-6600) Sample chaincode for private data +* [21444ab](https://github.com/hyperledger/fabric-samples/commit/21444ab) [FAB-10297](https://jira.hyperledger.org/browse/FAB-10297) replace fabric-preload.sh +* [3c32c52](https://github.com/hyperledger/fabric-samples/commit/3c32c52) [FAB-10346](https://jira.hyperledger.org/browse/FAB-10346) Ensure peers are in sync in eyfn +* [2f30504](https://github.com/hyperledger/fabric-samples/commit/2f30504) [FAB-10340](https://jira.hyperledger.org/browse/FAB-10340) Fix broken link in chaincode-docker-devmode +* [a603655](https://github.com/hyperledger/fabric-samples/commit/a603655) [FAB-10306](https://jira.hyperledger.org/browse/FAB-10306) Fix config in balance-transfer +* [1cd059e](https://github.com/hyperledger/fabric-samples/commit/1cd059e) [FAB-8612](https://jira.hyperledger.org/browse/FAB-8612) Cleanup eyfn/byfn with --remove-orphans +* [3031a8c](https://github.com/hyperledger/fabric-samples/commit/3031a8c) [FAB-10235](https://jira.hyperledger.org/browse/FAB-10235) Update BYFN to use V1_2 capability +* [5cf1944](https://github.com/hyperledger/fabric-samples/commit/5cf1944) [FAB-10185](https://jira.hyperledger.org/browse/FAB-10185) Reorder for logical consistency +* [e2f4f20](https://github.com/hyperledger/fabric-samples/commit/e2f4f20) [FAB-10176](https://jira.hyperledger.org/browse/FAB-10176) Fix invalid configtx.yaml documents +* [cad2b98](https://github.com/hyperledger/fabric-samples/commit/cad2b98) [FAB-10073](https://jira.hyperledger.org/browse/FAB-10073) Fix broken link in samples/fabric-ca/README +* [ace2e5a](https://github.com/hyperledger/fabric-samples/commit/ace2e5a) [FAB-10021](https://jira.hyperledger.org/browse/FAB-10021) Typo in balance-transfer/testAPIs.sh +* [fa1d899](https://github.com/hyperledger/fabric-samples/commit/fa1d899) [FAB-9893](https://jira.hyperledger.org/browse/FAB-9893) Add .gitreview file for quicker Gerrit setup +* [98561e0](https://github.com/hyperledger/fabric-samples/commit/98561e0) [FAB-9552](https://jira.hyperledger.org/browse/FAB-9552) Fix access of an un-declared variable +* [b5dad8c](https://github.com/hyperledger/fabric-samples/commit/b5dad8c) [FAB-9317](https://jira.hyperledger.org/browse/FAB-9317) Update first-network with multi endorse +* [146f7bc](https://github.com/hyperledger/fabric-samples/commit/146f7bc) [FAB-9572](https://jira.hyperledger.org/browse/FAB-9572) Fix README to match new version of BYFN +* [010fcb5](https://github.com/hyperledger/fabric-samples/commit/010fcb5) [FAB-9326](https://jira.hyperledger.org/browse/FAB-9326) Clean up byfn.sh, drop "-m" option +* [2d6386c](https://github.com/hyperledger/fabric-samples/commit/2d6386c) [FAB-8245](https://jira.hyperledger.org/browse/FAB-8245) change first network according to the fix +* [3a5108f](https://github.com/hyperledger/fabric-samples/commit/3a5108f) [FAB-9475](https://jira.hyperledger.org/browse/FAB-9475) Fix a dead link in README +* [77a6568](https://github.com/hyperledger/fabric-samples/commit/77a6568) [FAB-9294](https://jira.hyperledger.org/browse/FAB-9294) eliminate excess noise in BYFN +* [41f5ab8](https://github.com/hyperledger/fabric-samples/commit/41f5ab8) [FAB-9406](https://jira.hyperledger.org/browse/FAB-9406) Typo in byfn.sh +* [f5c2eb8](https://github.com/hyperledger/fabric-samples/commit/f5c2eb8) [FAB-9362](https://jira.hyperledger.org/browse/FAB-9362) add CONTRIBUTING.md and CODE_OF_CONDUCT.md +* [9d518fb](https://github.com/hyperledger/fabric-samples/commit/9d518fb) [FAB-9330](https://jira.hyperledger.org/browse/FAB-9330) Refactor top-level .gitignore into subdirs +* [a080da3](https://github.com/hyperledger/fabric-samples/commit/a080da3) [FAB-8958](https://jira.hyperledger.org/browse/FAB-8958) Add org3 removal to byfn.sh +* [8fc0865](https://github.com/hyperledger/fabric-samples/commit/8fc0865) [FAB-9185](https://jira.hyperledger.org/browse/FAB-9185) add /config to .gitignore +* [680ff01](https://github.com/hyperledger/fabric-samples/commit/680ff01) [FAB-8600](https://jira.hyperledger.org/browse/FAB-8600)Clear hyperledger-related containers only +* [4f97717](https://github.com/hyperledger/fabric-samples/commit/4f97717) [FAB-8947](https://jira.hyperledger.org/browse/FAB-8947) Fabric-Samples remove package lock +* [fcf62ad](https://github.com/hyperledger/fabric-samples/commit/fcf62ad) [FAB-8265](https://jira.hyperledger.org/browse/FAB-8265) Fixed spelling error in registerUser +* [e4d7760](https://github.com/hyperledger/fabric-samples/commit/e4d7760) [ [FAB-8730](https://jira.hyperledger.org/browse/FAB-8730) ] hyphen breaks fabric-samples +* [cf79cd1](https://github.com/hyperledger/fabric-samples/commit/cf79cd1) [FAB-7584](https://jira.hyperledger.org/browse/FAB-7584) Removes copy of creds to keystore + +## "v1.1.0" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [8a458b5](https://github.com/hyperledger/fabric-samples/commit/8a458b5) [FAB-12056](https://jira.hyperledger.org/browse/FAB-12056) Private marbles cc use transient data +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link +* [461b6ab](https://github.com/hyperledger/fabric-samples/commit/461b6ab) FABC-781 Remove fabric-ca sample +* [e9b9477](https://github.com/hyperledger/fabric-samples/commit/e9b9477) [FAB-13372](https://jira.hyperledger.org/browse/FAB-13372) Fabric-Samples return error msg +* [e3da220](https://github.com/hyperledger/fabric-samples/commit/e3da220) [FAB-13433](https://jira.hyperledger.org/browse/FAB-13433) - Update Jenkinsfile configuration +* [33db64e](https://github.com/hyperledger/fabric-samples/commit/33db64e) Configure Stale ProBot +* [5cd277f](https://github.com/hyperledger/fabric-samples/commit/5cd277f) [FAB-11951](https://jira.hyperledger.org/browse/FAB-11951) Interest-rate swap example for SBE +* [9567985](https://github.com/hyperledger/fabric-samples/commit/9567985) [FAB-13407](https://jira.hyperledger.org/browse/FAB-13407) Align fabric-samples with 1.4.0-rc2 release +* [c7572aa](https://github.com/hyperledger/fabric-samples/commit/c7572aa) [FAB-13305](https://jira.hyperledger.org/browse/FAB-13305) Update scripts to pull latest artifacts +* [ab46e35](https://github.com/hyperledger/fabric-samples/commit/ab46e35) [FAB-13283](https://jira.hyperledger.org/browse/FAB-13283) Update sample code for commercial paper +* [f677821](https://github.com/hyperledger/fabric-samples/commit/f677821) [FAB-13232](https://jira.hyperledger.org/browse/FAB-13232) fix peer node start command +* [6a7472e](https://github.com/hyperledger/fabric-samples/commit/6a7472e) [FAB-13126](https://jira.hyperledger.org/browse/FAB-13126) Align fabric-samples with 1.4.0-rc1 release +* [445ccbc](https://github.com/hyperledger/fabric-samples/commit/445ccbc) [FAB-12880](https://jira.hyperledger.org/browse/FAB-12880) Move old prog model samples for FabCar +* [5be62b5](https://github.com/hyperledger/fabric-samples/commit/5be62b5) [FAB-13207](https://jira.hyperledger.org/browse/FAB-13207) Remove incorrect discovery options +* [fdbd92d](https://github.com/hyperledger/fabric-samples/commit/fdbd92d) [FAB-13206](https://jira.hyperledger.org/browse/FAB-13206) Remove dependencies on fabric-client +* [eff0046](https://github.com/hyperledger/fabric-samples/commit/eff0046) [FAB-12877](https://jira.hyperledger.org/browse/FAB-12877) Add fabcar app using new prog model (JS) +* [c184196](https://github.com/hyperledger/fabric-samples/commit/c184196) [FAB-12878](https://jira.hyperledger.org/browse/FAB-12878) Add fabcar app using new prog model (TS) +* [e1a39e6](https://github.com/hyperledger/fabric-samples/commit/e1a39e6) [FAB-12724](https://jira.hyperledger.org/browse/FAB-12724) Upgrade from 1.3.x to 1.4.0 +* [c21bbba](https://github.com/hyperledger/fabric-samples/commit/c21bbba) Update samples to use new logging env variables +* [7ad9f19](https://github.com/hyperledger/fabric-samples/commit/7ad9f19) [FAB-13011](https://jira.hyperledger.org/browse/FAB-13011) add kafka consensus type to byfn sample +* [33f064f](https://github.com/hyperledger/fabric-samples/commit/33f064f) [FAB-13170](https://jira.hyperledger.org/browse/FAB-13170) Add memberOnlyRead to marbles sample +* [928b72b](https://github.com/hyperledger/fabric-samples/commit/928b72b) [FAB-12875](https://jira.hyperledger.org/browse/FAB-12875) Add automated tests for fabcar sample +* [5c087f1](https://github.com/hyperledger/fabric-samples/commit/5c087f1) [FAB-13046](https://jira.hyperledger.org/browse/FAB-13046) Update TypeScript contract dependencies +* [3748983](https://github.com/hyperledger/fabric-samples/commit/3748983) [FAB-12879](https://jira.hyperledger.org/browse/FAB-12879) Update fabcar script for new contracts +* [4fb3b57](https://github.com/hyperledger/fabric-samples/commit/4fb3b57) [FAB-12852](https://jira.hyperledger.org/browse/FAB-12852) Add fabcar contract w/ new prog model (TS) +* [9facb42](https://github.com/hyperledger/fabric-samples/commit/9facb42) [FAB-12851](https://jira.hyperledger.org/browse/FAB-12851) Add fabcar contract w/ new prog model (JS) +* [e67fcf1](https://github.com/hyperledger/fabric-samples/commit/e67fcf1) [FAB-12322](https://jira.hyperledger.org/browse/FAB-12322) Update commercial-paper sample +* [fd6e2c4](https://github.com/hyperledger/fabric-samples/commit/fd6e2c4) [FAB-12703](https://jira.hyperledger.org/browse/FAB-12703) Fix misspelling "lauches" +* [c05f172](https://github.com/hyperledger/fabric-samples/commit/c05f172) [FAB-12608](https://jira.hyperledger.org/browse/FAB-12608) Update pipeline script +* [286861e](https://github.com/hyperledger/fabric-samples/commit/286861e) [FAB-12371](https://jira.hyperledger.org/browse/FAB-12371)Fix the abac sample to use new cid package +* [24c5e47](https://github.com/hyperledger/fabric-samples/commit/24c5e47) [FAB-12026](https://jira.hyperledger.org/browse/FAB-12026) pagination samples for node marbles02 +* [6dc5ce5](https://github.com/hyperledger/fabric-samples/commit/6dc5ce5) [FAB-12587](https://jira.hyperledger.org/browse/FAB-12587) Fix for Query Block by block hash API +* [df311ce](https://github.com/hyperledger/fabric-samples/commit/df311ce) [FAB-12173](https://jira.hyperledger.org/browse/FAB-12173) balance-transfer: Update anchor peers +* [c925148](https://github.com/hyperledger/fabric-samples/commit/c925148) [FAB-12415](https://jira.hyperledger.org/browse/FAB-12415) samples for 1.3.0 (master cleanup) +* [3a12c60](https://github.com/hyperledger/fabric-samples/commit/3a12c60) [FAB-12275](https://jira.hyperledger.org/browse/FAB-12275) Fix the warn in creating genesis block +* [c6f6324](https://github.com/hyperledger/fabric-samples/commit/c6f6324) [FAB-12257](https://jira.hyperledger.org/browse/FAB-12257) allow balance-transfer for doscovery +* [4445e8d](https://github.com/hyperledger/fabric-samples/commit/4445e8d) [FAB-12272](https://jira.hyperledger.org/browse/FAB-12272) Increase MAX_RETRY to 10 +* [33d333f](https://github.com/hyperledger/fabric-samples/commit/33d333f) [FAB-12190](https://jira.hyperledger.org/browse/FAB-12190) Update stable version in CI scripts +* [edee638](https://github.com/hyperledger/fabric-samples/commit/edee638) [FAB-12184](https://jira.hyperledger.org/browse/FAB-12184) Prepare fabric-samples for 1.3.0-rc1 +* [514d456](https://github.com/hyperledger/fabric-samples/commit/514d456) [FAB-12170](https://jira.hyperledger.org/browse/FAB-12170) Fix dependency check in java chaincode +* [9f80e47](https://github.com/hyperledger/fabric-samples/commit/9f80e47) [FAB-12119](https://jira.hyperledger.org/browse/FAB-12119) Fix groupId in java chaincodes +* [3237229](https://github.com/hyperledger/fabric-samples/commit/3237229) [FAB-12073](https://jira.hyperledger.org/browse/FAB-12073) Fix Org3 peers CouchDB config. +* [eece3d8](https://github.com/hyperledger/fabric-samples/commit/eece3d8) [FAB-12106](https://jira.hyperledger.org/browse/FAB-12106) Update fabric-ca build scripts +* [f62952f](https://github.com/hyperledger/fabric-samples/commit/f62952f) [FABC-131] Change fabric-ca sample to build images +* [4089786](https://github.com/hyperledger/fabric-samples/commit/4089786) [FAB-11867](https://jira.hyperledger.org/browse/FAB-11867) Develop Apps:Sample pt 2 -- application +* [9ee57c6](https://github.com/hyperledger/fabric-samples/commit/9ee57c6) [FAB-11778](https://jira.hyperledger.org/browse/FAB-11778) Upgrade to v1.3.x from v1.2.x in byfn +* [e7a1b76](https://github.com/hyperledger/fabric-samples/commit/e7a1b76) [FAB-9386](https://jira.hyperledger.org/browse/FAB-9386) Remove Marbles sample reference to Fauxton +* [9c6acee](https://github.com/hyperledger/fabric-samples/commit/9c6acee) [FAB-12022](https://jira.hyperledger.org/browse/FAB-12022) Fix CI by increasing couchdb timeout +* [4030ebd](https://github.com/hyperledger/fabric-samples/commit/4030ebd) [FAB-11397](https://jira.hyperledger.org/browse/FAB-11397) Adding java cc +* [d776651](https://github.com/hyperledger/fabric-samples/commit/d776651) [FAB-11723](https://jira.hyperledger.org/browse/FAB-11723) Developing Apps: Sample pt 1 -- contract +* [cbbbc78](https://github.com/hyperledger/fabric-samples/commit/cbbbc78) [FAB-11577](https://jira.hyperledger.org/browse/FAB-11577) Fix balance transfer to install Chaincode +* [bfdc0b6](https://github.com/hyperledger/fabric-samples/commit/bfdc0b6) [FAB-11518](https://jira.hyperledger.org/browse/FAB-11518) +* [5930dfc](https://github.com/hyperledger/fabric-samples/commit/5930dfc) [FAB-11488](https://jira.hyperledger.org/browse/FAB-11488) Update CI script +* [ca6959c](https://github.com/hyperledger/fabric-samples/commit/ca6959c) [FAB-11311](https://jira.hyperledger.org/browse/FAB-11311) Update fabric image version +* [0ca9e6e](https://github.com/hyperledger/fabric-samples/commit/0ca9e6e) FABN-833 Update Jenkinsfile to capture build artifacts +* [a4a15cb](https://github.com/hyperledger/fabric-samples/commit/a4a15cb) [FAB-11220](https://jira.hyperledger.org/browse/FAB-11220) Samples - remove EventHub +* [c4bdc68](https://github.com/hyperledger/fabric-samples/commit/c4bdc68) [FAB-8479](https://jira.hyperledger.org/browse/FAB-8479) Added Endorsement policy +* [6edd320](https://github.com/hyperledger/fabric-samples/commit/6edd320) [FAB-9297](https://jira.hyperledger.org/browse/FAB-9297) fix README links and update bootstrap.sh +* [75e2931](https://github.com/hyperledger/fabric-samples/commit/75e2931) [FAB-10811](https://jira.hyperledger.org/browse/FAB-10811) fabric-ca sample is broken on v1.2 +* [ad40e29](https://github.com/hyperledger/fabric-samples/commit/ad40e29) [FAB-10732](https://jira.hyperledger.org/browse/FAB-10732) BYFN upgrade to v1.2 +* [20ad472](https://github.com/hyperledger/fabric-samples/commit/20ad472) [FAB-10801](https://jira.hyperledger.org/browse/FAB-10801) format and styling for shell artifacts +* [e95210e](https://github.com/hyperledger/fabric-samples/commit/e95210e) [FAB-10074](https://jira.hyperledger.org/browse/FAB-10074) CI Script for pipeline project type +* [5956178](https://github.com/hyperledger/fabric-samples/commit/5956178) [FAB-6600](https://jira.hyperledger.org/browse/FAB-6600) Sample chaincode for private data +* [21444ab](https://github.com/hyperledger/fabric-samples/commit/21444ab) [FAB-10297](https://jira.hyperledger.org/browse/FAB-10297) replace fabric-preload.sh +* [3c32c52](https://github.com/hyperledger/fabric-samples/commit/3c32c52) [FAB-10346](https://jira.hyperledger.org/browse/FAB-10346) Ensure peers are in sync in eyfn +* [2f30504](https://github.com/hyperledger/fabric-samples/commit/2f30504) [FAB-10340](https://jira.hyperledger.org/browse/FAB-10340) Fix broken link in chaincode-docker-devmode +* [a603655](https://github.com/hyperledger/fabric-samples/commit/a603655) [FAB-10306](https://jira.hyperledger.org/browse/FAB-10306) Fix config in balance-transfer +* [1cd059e](https://github.com/hyperledger/fabric-samples/commit/1cd059e) [FAB-8612](https://jira.hyperledger.org/browse/FAB-8612) Cleanup eyfn/byfn with --remove-orphans +* [3031a8c](https://github.com/hyperledger/fabric-samples/commit/3031a8c) [FAB-10235](https://jira.hyperledger.org/browse/FAB-10235) Update BYFN to use V1_2 capability +* [5cf1944](https://github.com/hyperledger/fabric-samples/commit/5cf1944) [FAB-10185](https://jira.hyperledger.org/browse/FAB-10185) Reorder for logical consistency +* [e2f4f20](https://github.com/hyperledger/fabric-samples/commit/e2f4f20) [FAB-10176](https://jira.hyperledger.org/browse/FAB-10176) Fix invalid configtx.yaml documents +* [cad2b98](https://github.com/hyperledger/fabric-samples/commit/cad2b98) [FAB-10073](https://jira.hyperledger.org/browse/FAB-10073) Fix broken link in samples/fabric-ca/README +* [ace2e5a](https://github.com/hyperledger/fabric-samples/commit/ace2e5a) [FAB-10021](https://jira.hyperledger.org/browse/FAB-10021) Typo in balance-transfer/testAPIs.sh +* [fa1d899](https://github.com/hyperledger/fabric-samples/commit/fa1d899) [FAB-9893](https://jira.hyperledger.org/browse/FAB-9893) Add .gitreview file for quicker Gerrit setup +* [98561e0](https://github.com/hyperledger/fabric-samples/commit/98561e0) [FAB-9552](https://jira.hyperledger.org/browse/FAB-9552) Fix access of an un-declared variable +* [b5dad8c](https://github.com/hyperledger/fabric-samples/commit/b5dad8c) [FAB-9317](https://jira.hyperledger.org/browse/FAB-9317) Update first-network with multi endorse +* [146f7bc](https://github.com/hyperledger/fabric-samples/commit/146f7bc) [FAB-9572](https://jira.hyperledger.org/browse/FAB-9572) Fix README to match new version of BYFN +* [010fcb5](https://github.com/hyperledger/fabric-samples/commit/010fcb5) [FAB-9326](https://jira.hyperledger.org/browse/FAB-9326) Clean up byfn.sh, drop "-m" option +* [2d6386c](https://github.com/hyperledger/fabric-samples/commit/2d6386c) [FAB-8245](https://jira.hyperledger.org/browse/FAB-8245) change first network according to the fix +* [3a5108f](https://github.com/hyperledger/fabric-samples/commit/3a5108f) [FAB-9475](https://jira.hyperledger.org/browse/FAB-9475) Fix a dead link in README +* [77a6568](https://github.com/hyperledger/fabric-samples/commit/77a6568) [FAB-9294](https://jira.hyperledger.org/browse/FAB-9294) eliminate excess noise in BYFN +* [41f5ab8](https://github.com/hyperledger/fabric-samples/commit/41f5ab8) [FAB-9406](https://jira.hyperledger.org/browse/FAB-9406) Typo in byfn.sh +* [f5c2eb8](https://github.com/hyperledger/fabric-samples/commit/f5c2eb8) [FAB-9362](https://jira.hyperledger.org/browse/FAB-9362) add CONTRIBUTING.md and CODE_OF_CONDUCT.md +* [9d518fb](https://github.com/hyperledger/fabric-samples/commit/9d518fb) [FAB-9330](https://jira.hyperledger.org/browse/FAB-9330) Refactor top-level .gitignore into subdirs +* [a080da3](https://github.com/hyperledger/fabric-samples/commit/a080da3) [FAB-8958](https://jira.hyperledger.org/browse/FAB-8958) Add org3 removal to byfn.sh +* [8fc0865](https://github.com/hyperledger/fabric-samples/commit/8fc0865) [FAB-9185](https://jira.hyperledger.org/browse/FAB-9185) add /config to .gitignore +* [680ff01](https://github.com/hyperledger/fabric-samples/commit/680ff01) [FAB-8600](https://jira.hyperledger.org/browse/FAB-8600)Clear hyperledger-related containers only +* [4f97717](https://github.com/hyperledger/fabric-samples/commit/4f97717) [FAB-8947](https://jira.hyperledger.org/browse/FAB-8947) Fabric-Samples remove package lock +* [fcf62ad](https://github.com/hyperledger/fabric-samples/commit/fcf62ad) [FAB-8265](https://jira.hyperledger.org/browse/FAB-8265) Fixed spelling error in registerUser +* [e4d7760](https://github.com/hyperledger/fabric-samples/commit/e4d7760) [ [FAB-8730](https://jira.hyperledger.org/browse/FAB-8730) ] hyphen breaks fabric-samples +* [2bbb0a8](https://github.com/hyperledger/fabric-samples/commit/2bbb0a8) [FAB-8630](https://jira.hyperledger.org/browse/FAB-8630) byfn failing intermittently in CI +* [823fb6b](https://github.com/hyperledger/fabric-samples/commit/823fb6b) [ [FAB-8679](https://jira.hyperledger.org/browse/FAB-8679) ] Permit samples to use RootCAs +* [9f9fc7e](https://github.com/hyperledger/fabric-samples/commit/9f9fc7e) [FAB-8633](https://jira.hyperledger.org/browse/FAB-8633) Correct revoked error check +* [f3b55c9](https://github.com/hyperledger/fabric-samples/commit/f3b55c9) [FAB-8621](https://jira.hyperledger.org/browse/FAB-8621) Remove Marbles index json data wrapper +* [f110a6e](https://github.com/hyperledger/fabric-samples/commit/f110a6e) [FAB-8602](https://jira.hyperledger.org/browse/FAB-8602) Add volumes to first-network e2e yaml +* [7362928](https://github.com/hyperledger/fabric-samples/commit/7362928) [FAB-8567](https://jira.hyperledger.org/browse/FAB-8567) Alt: Always use volumes for ledger (m) +* [afb3d62](https://github.com/hyperledger/fabric-samples/commit/afb3d62) [FAB-8561](https://jira.hyperledger.org/browse/FAB-8561) Add note to readthedocs link in README +* [10526d5](https://github.com/hyperledger/fabric-samples/commit/10526d5) [FAB-8564](https://jira.hyperledger.org/browse/FAB-8564) add debug commands to byfn +* [e73a481](https://github.com/hyperledger/fabric-samples/commit/e73a481) [FAB-8568](https://jira.hyperledger.org/browse/FAB-8568) BYFN: Fix IMAGE_TAG for couchdb +* [ffd7a25](https://github.com/hyperledger/fabric-samples/commit/ffd7a25) [FAB-6400](https://jira.hyperledger.org/browse/FAB-6400) Balance-transfer filtered events +* [cba57da](https://github.com/hyperledger/fabric-samples/commit/cba57da) [FAB-8165](https://jira.hyperledger.org/browse/FAB-8165) Adding upgrade function to byfn +* [c6166d6](https://github.com/hyperledger/fabric-samples/commit/c6166d6) [FAB-8540](https://jira.hyperledger.org/browse/FAB-8540) Add ledger persistance to first-network +* [77e74b7](https://github.com/hyperledger/fabric-samples/commit/77e74b7) [FAB-8497](https://jira.hyperledger.org/browse/FAB-8497) Download images required for fabric-ca +* [2bed1ef](https://github.com/hyperledger/fabric-samples/commit/2bed1ef) [FAB-8539](https://jira.hyperledger.org/browse/FAB-8539) Add version checking to first-network +* [7b7fc09](https://github.com/hyperledger/fabric-samples/commit/7b7fc09) [FAB-8496](https://jira.hyperledger.org/browse/FAB-8496) allow modification of affiliations +* [981efba](https://github.com/hyperledger/fabric-samples/commit/981efba) [FAB-8503](https://jira.hyperledger.org/browse/FAB-8503) Prevent CLI container from exiting +* [c93268f](https://github.com/hyperledger/fabric-samples/commit/c93268f) [FAB-8518](https://jira.hyperledger.org/browse/FAB-8518) Not all compose files have IMAGE_TAG +* [4f2cd8d](https://github.com/hyperledger/fabric-samples/commit/4f2cd8d) [FAB-8445](https://jira.hyperledger.org/browse/FAB-8445) Adding IMAGE_TAG option to byfn +* [ca80163](https://github.com/hyperledger/fabric-samples/commit/ca80163) [FAB-8387](https://jira.hyperledger.org/browse/FAB-8387) Add gencrl to revoke command +* [e379ac5](https://github.com/hyperledger/fabric-samples/commit/e379ac5) [FAB-8407](https://jira.hyperledger.org/browse/FAB-8407) Fix TLS bad cert error +* [02ca1dc](https://github.com/hyperledger/fabric-samples/commit/02ca1dc) [FAB-7494](https://jira.hyperledger.org/browse/FAB-7494) Fix bootstrap peer config +* [41e144f](https://github.com/hyperledger/fabric-samples/commit/41e144f) [FAB-8386](https://jira.hyperledger.org/browse/FAB-8386) eyfn fails to execute invoke on org3 +* [4ab098f](https://github.com/hyperledger/fabric-samples/commit/4ab098f) [FAB-8327](https://jira.hyperledger.org/browse/FAB-8327) Change eyfn.sh to use configtxlator cli +* [24f35c1](https://github.com/hyperledger/fabric-samples/commit/24f35c1) [FAB-7750](https://jira.hyperledger.org/browse/FAB-7750) first network with support to [FAB-5664](https://jira.hyperledger.org/browse/FAB-5664) +* [305d6f4](https://github.com/hyperledger/fabric-samples/commit/305d6f4) [FAB-8238](https://jira.hyperledger.org/browse/FAB-8238)wrong orderer/peer type in fabric-ca sam +* [1d69e9e](https://github.com/hyperledger/fabric-samples/commit/1d69e9e) [FAB-7540](https://jira.hyperledger.org/browse/FAB-7540) Simplifies Reconfigure Your Network tutorial +* [0daa8bc](https://github.com/hyperledger/fabric-samples/commit/0daa8bc) [FAB-8122](https://jira.hyperledger.org/browse/FAB-8122) Updated README.md file- balancetransfer +* [652f074](https://github.com/hyperledger/fabric-samples/commit/652f074) [FAB-7342](https://jira.hyperledger.org/browse/FAB-7342) Enable client auth in fabric-ca sample +* [90c2bfd](https://github.com/hyperledger/fabric-samples/commit/90c2bfd) [FAB-6934](https://jira.hyperledger.org/browse/FAB-6934) fix devmode sample for v1.1 +* [2c0c2c7](https://github.com/hyperledger/fabric-samples/commit/2c0c2c7) [FAB-7910](https://jira.hyperledger.org/browse/FAB-7910) Clean up chaincode containers in fabric-ca +* [bbee1b2](https://github.com/hyperledger/fabric-samples/commit/bbee1b2) [FAB-7908](https://jira.hyperledger.org/browse/FAB-7908) Change hf.admin attr to admin +* [dd12f88](https://github.com/hyperledger/fabric-samples/commit/dd12f88) [FAB-7834](https://jira.hyperledger.org/browse/FAB-7834) Add couchdb index to marbles02 sample +* [25f6091](https://github.com/hyperledger/fabric-samples/commit/25f6091) [FAB-7836](https://jira.hyperledger.org/browse/FAB-7836) Fix "No identity type provided" Error +* [5eb2fb2](https://github.com/hyperledger/fabric-samples/commit/5eb2fb2) [FAB-7653](https://jira.hyperledger.org/browse/FAB-7653) Fix incorrect chaincode location +* [5a974a4](https://github.com/hyperledger/fabric-samples/commit/5a974a4) [FAB-7533](https://jira.hyperledger.org/browse/FAB-7533) Fix typo in balance-transfer sample +* [038c496](https://github.com/hyperledger/fabric-samples/commit/038c496) [FAB-7592](https://jira.hyperledger.org/browse/FAB-7592) Give hf.Registrar attrs to admins +* [cf79cd1](https://github.com/hyperledger/fabric-samples/commit/cf79cd1) [FAB-7584](https://jira.hyperledger.org/browse/FAB-7584) Removes copy of creds to keystore +* [a0c1687](https://github.com/hyperledger/fabric-samples/commit/a0c1687) [FAB-7487](https://jira.hyperledger.org/browse/FAB-7487) Fix typo in node/fabcar.js +* [1883ae9](https://github.com/hyperledger/fabric-samples/commit/1883ae9) [FAB-7527](https://jira.hyperledger.org/browse/FAB-7527) Improves BYFN +* [e848216](https://github.com/hyperledger/fabric-samples/commit/e848216) [FAB-7511](https://jira.hyperledger.org/browse/FAB-7511) clear crypto material after clearing network +* [54ffa5f](https://github.com/hyperledger/fabric-samples/commit/54ffa5f) [FAB-7241](https://jira.hyperledger.org/browse/FAB-7241) Fix chaincode-devmode +* [c446510](https://github.com/hyperledger/fabric-samples/commit/c446510) [FAB-6550](https://jira.hyperledger.org/browse/FAB-6550) Sample app written in typescript +* [69a127e](https://github.com/hyperledger/fabric-samples/commit/69a127e) [FAB-6254](https://jira.hyperledger.org/browse/FAB-6254) Fix the default CLI timeout +* [7428f64](https://github.com/hyperledger/fabric-samples/commit/7428f64) [FAB-7160](https://jira.hyperledger.org/browse/FAB-7160) Samples - readme sample commands +* [2474704](https://github.com/hyperledger/fabric-samples/commit/2474704) [FAB-6967](https://jira.hyperledger.org/browse/FAB-6967) Added steps to query by a revoked user +* [948e237](https://github.com/hyperledger/fabric-samples/commit/948e237) [FAB-5913](https://jira.hyperledger.org/browse/FAB-5913)balance-transfer:Fix query response message +* [7b76a7a](https://github.com/hyperledger/fabric-samples/commit/7b76a7a) [FAB-6632](https://jira.hyperledger.org/browse/FAB-6632) - Artifacts for BYFN reconfigure +* [24ef9da](https://github.com/hyperledger/fabric-samples/commit/24ef9da) [FAB-6902](https://jira.hyperledger.org/browse/FAB-6902) [FAB-6904](https://jira.hyperledger.org/browse/FAB-6904) correct the sample for FAB-6904 +* [fd795d2](https://github.com/hyperledger/fabric-samples/commit/fd795d2) [FAB-6745](https://jira.hyperledger.org/browse/FAB-6745) Fix timing issue in sample +* [a7be462](https://github.com/hyperledger/fabric-samples/commit/a7be462) [FAB-6895](https://jira.hyperledger.org/browse/FAB-6895) chaincode mounting issue +* [7c23985](https://github.com/hyperledger/fabric-samples/commit/7c23985) [FAB-5221](https://jira.hyperledger.org/browse/FAB-5221) generateCerts should delete crypto +* [1961835](https://github.com/hyperledger/fabric-samples/commit/1961835) [FAB-6870](https://jira.hyperledger.org/browse/FAB-6870) Update node modules versions +* [bb3ac84](https://github.com/hyperledger/fabric-samples/commit/bb3ac84) [FAB-5363](https://jira.hyperledger.org/browse/FAB-5363) fabric-samples update balance +* [6b2799e](https://github.com/hyperledger/fabric-samples/commit/6b2799e) [FAB-6568](https://jira.hyperledger.org/browse/FAB-6568) Fabric-Samples - update fabcar +* [fafae55](https://github.com/hyperledger/fabric-samples/commit/fafae55) [FAB-6779](https://jira.hyperledger.org/browse/FAB-6779) Fix the error in fabric-ca +* [caf5c33](https://github.com/hyperledger/fabric-samples/commit/caf5c33) [FAB-6050](https://jira.hyperledger.org/browse/FAB-6050) Adding fabric-ca sample +* [44c204d](https://github.com/hyperledger/fabric-samples/commit/44c204d) [FAB-5898](https://jira.hyperledger.org/browse/FAB-5898) porting samples to node.js chaincode +* [07a07d5](https://github.com/hyperledger/fabric-samples/commit/07a07d5) [FAB-6361](https://jira.hyperledger.org/browse/FAB-6361) Update license text in README +* [77b4090](https://github.com/hyperledger/fabric-samples/commit/77b4090) [FAB-5992](https://jira.hyperledger.org/browse/FAB-5992) Fix error in first-network dir + +## "v1.0.6" + +* [11e4c23](https://github.com/hyperledger/fabric-samples/commit/11e4c23) Update samples to use v2.0 or later dependencies (#161) +* [94beab7](https://github.com/hyperledger/fabric-samples/commit/94beab7) FABN-1531 Use v2.1.0 sdk-node modules +* [8820d2f](https://github.com/hyperledger/fabric-samples/commit/8820d2f) Fix commercial-paper README +* [aa9b577](https://github.com/hyperledger/fabric-samples/commit/aa9b577) Remove TLS enabled switch (#155) +* [381fb46](https://github.com/hyperledger/fabric-samples/commit/381fb46) upgraded abstore golang chaincode to use contract-api package (#154) +* [5e5d2c8](https://github.com/hyperledger/fabric-samples/commit/5e5d2c8) Update java chaincode to be compatible with doc and other implementations (#149) +* [c572c51](https://github.com/hyperledger/fabric-samples/commit/c572c51) Organize and Standardize `ci` Directory Content (#152) +* [aa40963](https://github.com/hyperledger/fabric-samples/commit/aa40963) Perform General Cleanup (#151) +* [da41afa](https://github.com/hyperledger/fabric-samples/commit/da41afa) Remove left over rm -rf command from BYFN (#148) +* [4bb48a9](https://github.com/hyperledger/fabric-samples/commit/4bb48a9) Jenkins no longer used (#145) +* [6f984e1](https://github.com/hyperledger/fabric-samples/commit/6f984e1) Bump acorn from 6.4.0 to 6.4.1 in /fabcar/javascript (#144) +* [b155620](https://github.com/hyperledger/fabric-samples/commit/b155620) Remove redundant invoke command from test network (#142) +* [851933b](https://github.com/hyperledger/fabric-samples/commit/851933b) Add enrollUser files to commercial paper (#140) +* [87600bd](https://github.com/hyperledger/fabric-samples/commit/87600bd) [FAB-17268](https://jira.hyperledger.org/browse/FAB-17268) Move fabcar sample to test network (#103) +* [9397788](https://github.com/hyperledger/fabric-samples/commit/9397788) Wrong groupId on hyperledger fabric dependencies for java-application (#134) +* [92555fb](https://github.com/hyperledger/fabric-samples/commit/92555fb) Update README.md (#133) +* [59c6641](https://github.com/hyperledger/fabric-samples/commit/59c6641) Change Download Location of Fabric Binaries (#143) +* [1f283fc](https://github.com/hyperledger/fabric-samples/commit/1f283fc) init function does not exist on fabcar (#141) +* [defb6bb](https://github.com/hyperledger/fabric-samples/commit/defb6bb) [FAB-17656](https://jira.hyperledger.org/browse/FAB-17656) echo Generating channel.tx (#139) +* [4c7bab0](https://github.com/hyperledger/fabric-samples/commit/4c7bab0) fix: package seletor REGEX (#135) +* [db69c6f](https://github.com/hyperledger/fabric-samples/commit/db69c6f) Add fabcar external service sample (#136) +* [7f5f5e6](https://github.com/hyperledger/fabric-samples/commit/7f5f5e6) [FAB-17504](https://jira.hyperledger.org/browse/FAB-17504) add Organizations..OrdererEndpoints and remove Orderer.Addresses (#125) +* [f3fc08d](https://github.com/hyperledger/fabric-samples/commit/f3fc08d) Remove solo and kafka from test net configtx.yaml (#137) +* [e17574d](https://github.com/hyperledger/fabric-samples/commit/e17574d) Add CA's to docker test network (#124) +* [faac18e](https://github.com/hyperledger/fabric-samples/commit/faac18e) [FAB-17461](https://jira.hyperledger.org/browse/FAB-17461) Move off_chain_data sample to test network (#122) +* [121a44a](https://github.com/hyperledger/fabric-samples/commit/121a44a) [FAB-17460](https://jira.hyperledger.org/browse/FAB-17460) Move High Throughput sample to test network (#112) +* [a2f3a66](https://github.com/hyperledger/fabric-samples/commit/a2f3a66) Update docker image version +* [e5b898c](https://github.com/hyperledger/fabric-samples/commit/e5b898c) Revert "first-network/scripts/*: Make Chaincode name configurable (#118)" (#131) +* [9ef61e2](https://github.com/hyperledger/fabric-samples/commit/9ef61e2) first-network/scripts/*: Make Chaincode name configurable (#118) +* [e204ebb](https://github.com/hyperledger/fabric-samples/commit/e204ebb) Remove reference to 2.0 beta (#111) +* [3dbe116](https://github.com/hyperledger/fabric-samples/commit/3dbe116) [FAB-17456](https://jira.hyperledger.org/browse/FAB-17456) fabric-samples read ccp (#117) +* [965ed1f](https://github.com/hyperledger/fabric-samples/commit/965ed1f) [FAB-17498](https://jira.hyperledger.org/browse/FAB-17498) Beta Images removal, test test-network (#121) +* [403019e](https://github.com/hyperledger/fabric-samples/commit/403019e) [FAB-17495](https://jira.hyperledger.org/browse/FAB-17495) Remove Basic Network sample (#120) +* [883ef99](https://github.com/hyperledger/fabric-samples/commit/883ef99) [FAB-17457](https://jira.hyperledger.org/browse/FAB-17457) Script correction (#119) +* [b89ee34](https://github.com/hyperledger/fabric-samples/commit/b89ee34) Update Commercial Paper to v2.0 Lifecycle (#109) +* [4208644](https://github.com/hyperledger/fabric-samples/commit/4208644) [FAB-17478](https://jira.hyperledger.org/browse/FAB-17478) Update commercial paper to use go api v1.0.0 (#115) +* [0df5ed9](https://github.com/hyperledger/fabric-samples/commit/0df5ed9) [FAB-17477](https://jira.hyperledger.org/browse/FAB-17477) Update fabcar to use go api v1.0.0 (#116) +* [571733f](https://github.com/hyperledger/fabric-samples/commit/571733f) [FAB-17447](https://jira.hyperledger.org/browse/FAB-17447) Update to 2.0.0 Libraries +* [67b4ee7](https://github.com/hyperledger/fabric-samples/commit/67b4ee7) Add Org3 bugs in test network (#108) +* [5b93dd0](https://github.com/hyperledger/fabric-samples/commit/5b93dd0) [FAB-17140](https://jira.hyperledger.org/browse/FAB-17140) Add go commercial paper contract (#102) +* [4fe6a25](https://github.com/hyperledger/fabric-samples/commit/4fe6a25) [FABCI-482] Update Nexus URL's to Artifactory (#92) +* [1488fbb](https://github.com/hyperledger/fabric-samples/commit/1488fbb) Add 1.x versions of fabric to blacklisted versions +* [8ca279d](https://github.com/hyperledger/fabric-samples/commit/8ca279d) Add Support for Versioning NodeJS (#106) +* [b3b5267](https://github.com/hyperledger/fabric-samples/commit/b3b5267) [FAB-17243](https://jira.hyperledger.org/browse/FAB-17243) Add support for Fabric CA for Org3 on the (#91) +* [ce41ff7](https://github.com/hyperledger/fabric-samples/commit/ce41ff7) Remove references to vendoring chaincode from your gopath (#96) +* [4235d30](https://github.com/hyperledger/fabric-samples/commit/4235d30) [FAB-17306](https://jira.hyperledger.org/browse/FAB-17306) Fix artifact names in test-network (#97) +* [4c2a0a4](https://github.com/hyperledger/fabric-samples/commit/4c2a0a4) [FAB-16147](https://jira.hyperledger.org/browse/FAB-16147) Update Commercial Paper to work with v2 (#98) +* [6d9fd6f](https://github.com/hyperledger/fabric-samples/commit/6d9fd6f) Remove Gerrit reference +* [a026a4f](https://github.com/hyperledger/fabric-samples/commit/a026a4f) Fixed typo (#90) +* [cdb0e8b](https://github.com/hyperledger/fabric-samples/commit/cdb0e8b) TYPO (#89) +* [94ac8b6](https://github.com/hyperledger/fabric-samples/commit/94ac8b6) Update to use beta levels of modules (#88) +* [d848633](https://github.com/hyperledger/fabric-samples/commit/d848633) [FAB-16844](https://jira.hyperledger.org/browse/FAB-16844) Correct BYFN CC name +* [73267e1](https://github.com/hyperledger/fabric-samples/commit/73267e1) Fix test network bugs for adding org3 +* [5d58254](https://github.com/hyperledger/fabric-samples/commit/5d58254) [FAB-17145](https://jira.hyperledger.org/browse/FAB-17145) Add test network to Fabric Samples +* [e9f2957](https://github.com/hyperledger/fabric-samples/commit/e9f2957) [FAB-17062](https://jira.hyperledger.org/browse/FAB-17062) Fix typos in Commercial Paper readme +* [36694d0](https://github.com/hyperledger/fabric-samples/commit/36694d0) [FAB-17121](https://jira.hyperledger.org/browse/FAB-17121) Use new bootstrap config in orderer +* [429f087](https://github.com/hyperledger/fabric-samples/commit/429f087) update fabcar go to new programming model +* [1467086](https://github.com/hyperledger/fabric-samples/commit/1467086) Bump eslint-utils +* [33f349a](https://github.com/hyperledger/fabric-samples/commit/33f349a) Remove Stalebot +* [6af43bf](https://github.com/hyperledger/fabric-samples/commit/6af43bf) Change stalebot settings +* [4880401](https://github.com/hyperledger/fabric-samples/commit/4880401) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [fe96f60](https://github.com/hyperledger/fabric-samples/commit/fe96f60) [FAB-16850](https://jira.hyperledger.org/browse/FAB-16850) Set up CI with Azure Pipelines +* [81aabf4](https://github.com/hyperledger/fabric-samples/commit/81aabf4) [FAB-16849](https://jira.hyperledger.org/browse/FAB-16849) Various updates for Java version of FabCar +* [a42b858](https://github.com/hyperledger/fabric-samples/commit/a42b858) Update FabCar to reflect wallet API changes +* [890f9ea](https://github.com/hyperledger/fabric-samples/commit/890f9ea) [FAB-16713](https://jira.hyperledger.org/browse/FAB-16713) Fix npm audit warnings +* [e48e804](https://github.com/hyperledger/fabric-samples/commit/e48e804) [FAB-16776](https://jira.hyperledger.org/browse/FAB-16776) Move BYFN up to V2_0 capabilities +* [7b65a25](https://github.com/hyperledger/fabric-samples/commit/7b65a25) [IN-68] Add default GitHub SECURITY policy +* [408e0e8](https://github.com/hyperledger/fabric-samples/commit/408e0e8) [FAB-16619](https://jira.hyperledger.org/browse/FAB-16619) Fix the policy warning +* [670d446](https://github.com/hyperledger/fabric-samples/commit/670d446) [FAB-16668](https://jira.hyperledger.org/browse/FAB-16668) fabcar chaincode modify console output +* [f2939e2](https://github.com/hyperledger/fabric-samples/commit/f2939e2) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for Commercial Paper sample +* [3d19014](https://github.com/hyperledger/fabric-samples/commit/3d19014) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for FabCar sample +* [e2b7cb7](https://github.com/hyperledger/fabric-samples/commit/e2b7cb7) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Java 11 support for abstore sample +* [db48612](https://github.com/hyperledger/fabric-samples/commit/db48612) [FAB-6415](https://jira.hyperledger.org/browse/FAB-6415) Increase chaincode execute timeout +* [521a7ff](https://github.com/hyperledger/fabric-samples/commit/521a7ff) [FAB-16607](https://jira.hyperledger.org/browse/FAB-16607) Update FabCar to reflect CC updates +* [c13a5ec](https://github.com/hyperledger/fabric-samples/commit/c13a5ec) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [3fad853](https://github.com/hyperledger/fabric-samples/commit/3fad853) [FAB-16528](https://jira.hyperledger.org/browse/FAB-16528) marbles private chaincode sync up +* [8b9b82f](https://github.com/hyperledger/fabric-samples/commit/8b9b82f) [FAB-16489](https://jira.hyperledger.org/browse/FAB-16489) Add CODEOWNERS +* [a6ce915](https://github.com/hyperledger/fabric-samples/commit/a6ce915) [FAB-16487](https://jira.hyperledger.org/browse/FAB-16487) Update eslint +* [48082cf](https://github.com/hyperledger/fabric-samples/commit/48082cf) [FAB-16362](https://jira.hyperledger.org/browse/FAB-16362) adding chaincode excution comments +* [1d379f3](https://github.com/hyperledger/fabric-samples/commit/1d379f3) [FAB-16474](https://jira.hyperledger.org/browse/FAB-16474) marbles02 chaincode error +* [18712ca](https://github.com/hyperledger/fabric-samples/commit/18712ca) [FAB-16133](https://jira.hyperledger.org/browse/FAB-16133) Remove Solo consensus from BYFN +* [91c720a](https://github.com/hyperledger/fabric-samples/commit/91c720a) [FAB-16390](https://jira.hyperledger.org/browse/FAB-16390) Added filter for invalid transactions +* [1d3e267](https://github.com/hyperledger/fabric-samples/commit/1d3e267) Redirect samples to fabric-{chaincode,protos}-go +* [398a5b1](https://github.com/hyperledger/fabric-samples/commit/398a5b1) [FABCI-394] Remove AnsiColor Wrapper +* [ce154e0](https://github.com/hyperledger/fabric-samples/commit/ce154e0) [FAB-16310](https://jira.hyperledger.org/browse/FAB-16310) Vendor Go dependencies in all samples +* [6ea7c71](https://github.com/hyperledger/fabric-samples/commit/6ea7c71) [FAB-16285](https://jira.hyperledger.org/browse/FAB-16285) Update blacklisted versions in BYFN +* [86cd831](https://github.com/hyperledger/fabric-samples/commit/86cd831) [FAB-16284](https://jira.hyperledger.org/browse/FAB-16284) Remove E2E file and -f option from BYFN +* [0063abe](https://github.com/hyperledger/fabric-samples/commit/0063abe) Update stale script name in interest rate swaps +* [3907507](https://github.com/hyperledger/fabric-samples/commit/3907507) [FAB-16277](https://jira.hyperledger.org/browse/FAB-16277) Update BYFN w/ Raft ports in Docker network +* [33b0065](https://github.com/hyperledger/fabric-samples/commit/33b0065) [FAB-14813](https://jira.hyperledger.org/browse/FAB-14813) Channel event sample in fabric-samples +* [b62d5bd](https://github.com/hyperledger/fabric-samples/commit/b62d5bd) [FAB-16132](https://jira.hyperledger.org/browse/FAB-16132) Remove Kafka consensus from BYFN +* [9b14525](https://github.com/hyperledger/fabric-samples/commit/9b14525) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Update Commercial Paper for Java +* [4158877](https://github.com/hyperledger/fabric-samples/commit/4158877) [FAB-16232](https://jira.hyperledger.org/browse/FAB-16232) Remove FabToken sample +* [b6380cc](https://github.com/hyperledger/fabric-samples/commit/b6380cc) [FAB-16198](https://jira.hyperledger.org/browse/FAB-16198) Run "go mod vendor" for FabCar Go contract +* [639848a](https://github.com/hyperledger/fabric-samples/commit/639848a) [FAB-16197](https://jira.hyperledger.org/browse/FAB-16197) Add connection profiles to .gitignore +* [3996db5](https://github.com/hyperledger/fabric-samples/commit/3996db5) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) abstore node -> javascript +* [14ac271](https://github.com/hyperledger/fabric-samples/commit/14ac271) [FAB-12219](https://jira.hyperledger.org/browse/FAB-12219) marbles02 node -> javascript +* [13f16e5](https://github.com/hyperledger/fabric-samples/commit/13f16e5) [FGJ-4] CI tests for FabCar Java sample +* [171a7d2](https://github.com/hyperledger/fabric-samples/commit/171a7d2) FGJ-4 Fabcar sample +* [868f9d0](https://github.com/hyperledger/fabric-samples/commit/868f9d0) [FAB-15625](https://jira.hyperledger.org/browse/FAB-15625) Add UT for Simple Asset Chaincode +* [597d150](https://github.com/hyperledger/fabric-samples/commit/597d150) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [583ff8f](https://github.com/hyperledger/fabric-samples/commit/583ff8f) Use renamed CheckCommitReadiness function +* [750f937](https://github.com/hyperledger/fabric-samples/commit/750f937) [FAB-15213](https://jira.hyperledger.org/browse/FAB-15213) Add Java FabCar sample contract +* [abbda95](https://github.com/hyperledger/fabric-samples/commit/abbda95) [FAB-15897](https://jira.hyperledger.org/browse/FAB-15897) Improve FabCar test logging +* [dd8150a](https://github.com/hyperledger/fabric-samples/commit/dd8150a) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove versions from fabric-samples readme +* [1387aa8](https://github.com/hyperledger/fabric-samples/commit/1387aa8) [FAB-15927](https://jira.hyperledger.org/browse/FAB-15927) Better expression for golang +* [61c33d3](https://github.com/hyperledger/fabric-samples/commit/61c33d3) [FAB-15973](https://jira.hyperledger.org/browse/FAB-15973) use --output json on simulatecommit +* [8bbdd0f](https://github.com/hyperledger/fabric-samples/commit/8bbdd0f) [FAB-15716](https://jira.hyperledger.org/browse/FAB-15716) Fix instructions for dev-mode +* [0254d67](https://github.com/hyperledger/fabric-samples/commit/0254d67) QueryApprovalStatus -> SimulateCommitChaincodeDef +* [c57d67c](https://github.com/hyperledger/fabric-samples/commit/c57d67c) [FAB-15782](https://jira.hyperledger.org/browse/FAB-15782) Sample Go CC should include deps +* [6ba5a19](https://github.com/hyperledger/fabric-samples/commit/6ba5a19) Update to Go 1.12.5 in ci.properties +* [1774a25](https://github.com/hyperledger/fabric-samples/commit/1774a25) [FAB-15723](https://jira.hyperledger.org/browse/FAB-15723) Fix script and instruction with ccenv +* [6ae711c](https://github.com/hyperledger/fabric-samples/commit/6ae711c) [FAB-15717](https://jira.hyperledger.org/browse/FAB-15717) fix Error Unexpected end of JSON input +* [5be56d3](https://github.com/hyperledger/fabric-samples/commit/5be56d3) [FAB-15104](https://jira.hyperledger.org/browse/FAB-15104) Remove scripts/bootstrap.sh +* [779f8f3](https://github.com/hyperledger/fabric-samples/commit/779f8f3) [FAB-15649](https://jira.hyperledger.org/browse/FAB-15649)Fix Fabcar to install Chaincode on all peers +* [7c5f5d3](https://github.com/hyperledger/fabric-samples/commit/7c5f5d3) [FAB-15199](https://jira.hyperledger.org/browse/FAB-15199) Update interest rate sample +* [f0dca20](https://github.com/hyperledger/fabric-samples/commit/f0dca20) [FAB-14532](https://jira.hyperledger.org/browse/FAB-14532) Remove LL FabCar sample +* [1ed1a10](https://github.com/hyperledger/fabric-samples/commit/1ed1a10) [FAB-15573](https://jira.hyperledger.org/browse/FAB-15573) Fix typo in fabric-samples-ci.md +* [2e7fec9](https://github.com/hyperledger/fabric-samples/commit/2e7fec9) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [1e9e4c4](https://github.com/hyperledger/fabric-samples/commit/1e9e4c4) [FAB-9329](https://jira.hyperledger.org/browse/FAB-9329) Remove the unused variable in BYFN/EYFN +* [964c09f](https://github.com/hyperledger/fabric-samples/commit/964c09f) [FAB-15601](https://jira.hyperledger.org/browse/FAB-15601) BYFN: Fix MAX_RETRY for couchdb +* [41dca99](https://github.com/hyperledger/fabric-samples/commit/41dca99) [FAB-15127](https://jira.hyperledger.org/browse/FAB-15127) Update high throughput sample +* [3fe014a](https://github.com/hyperledger/fabric-samples/commit/3fe014a) Use official CouchDB image +* [f2d0fa0](https://github.com/hyperledger/fabric-samples/commit/f2d0fa0) [FAB-14487](https://jira.hyperledger.org/browse/FAB-14487) Make FabCar use BYFN, not basic-network +* [e9c3649](https://github.com/hyperledger/fabric-samples/commit/e9c3649) [FAB-15276](https://jira.hyperledger.org/browse/FAB-15276) Fix license statements +* [fbe4036](https://github.com/hyperledger/fabric-samples/commit/fbe4036) [FAB-14486](https://jira.hyperledger.org/browse/FAB-14486) Extend BYFN to opt skip chaincode deploy +* [0c4141f](https://github.com/hyperledger/fabric-samples/commit/0c4141f) [FAB-14485](https://jira.hyperledger.org/browse/FAB-14485) Extend BYFN to opt inc cert authorities +* [529b83b](https://github.com/hyperledger/fabric-samples/commit/529b83b) [FAB-14330](https://jira.hyperledger.org/browse/FAB-14330) Add connection profiles for BYFN and EYFN +* [2c21c83](https://github.com/hyperledger/fabric-samples/commit/2c21c83) [FABN-1184] Update fabtoken/README.md +* [5056a23](https://github.com/hyperledger/fabric-samples/commit/5056a23) [FABN-1184] Add CI script for fabtoken sample app +* [5d6db95](https://github.com/hyperledger/fabric-samples/commit/5d6db95) Update maintainers for fabric-samples +* [f527815](https://github.com/hyperledger/fabric-samples/commit/f527815) [FAB-15119](https://jira.hyperledger.org/browse/FAB-15119) Fix BYFN with Java chaincode +* [8245252](https://github.com/hyperledger/fabric-samples/commit/8245252) [FABN-1184] Implement fabtoken sample app +* [1bd1c2f](https://github.com/hyperledger/fabric-samples/commit/1bd1c2f) FABCI-284 Update CI Pipeline script +* [c24abf9](https://github.com/hyperledger/fabric-samples/commit/c24abf9) [FAB-15022](https://jira.hyperledger.org/browse/FAB-15022) Basic-network support for new lifecycle +* [b64fd45](https://github.com/hyperledger/fabric-samples/commit/b64fd45) [FAB-15051](https://jira.hyperledger.org/browse/FAB-15051) delStandard() function for high-throughput +* [3e68a7e](https://github.com/hyperledger/fabric-samples/commit/3e68a7e) [FAB-14784](https://jira.hyperledger.org/browse/FAB-14784) Remove balance-transfer +* [eb3fe08](https://github.com/hyperledger/fabric-samples/commit/eb3fe08) [FAB-14779](https://jira.hyperledger.org/browse/FAB-14779) QueryApprovalStatus step in byfn +* [2777429](https://github.com/hyperledger/fabric-samples/commit/2777429) [FAB-14711](https://jira.hyperledger.org/browse/FAB-14711) update byfn with new lifecycle +* [aec3389](https://github.com/hyperledger/fabric-samples/commit/aec3389) [FAB-12215](https://jira.hyperledger.org/browse/FAB-12215)WYFA:Remove chainId in tx proposal request +* [b5d5026](https://github.com/hyperledger/fabric-samples/commit/b5d5026) [FAB-14633](https://jira.hyperledger.org/browse/FAB-14633) Remove apt-get from eyfn.sh +* [efaadd3](https://github.com/hyperledger/fabric-samples/commit/efaadd3) [FAB-14531](https://jira.hyperledger.org/browse/FAB-14531) BYFN Raft with 5 nodes +* [d63047c](https://github.com/hyperledger/fabric-samples/commit/d63047c) [FAB-14444](https://jira.hyperledger.org/browse/FAB-14444) +* [7e3d428](https://github.com/hyperledger/fabric-samples/commit/7e3d428) [FAB-14369](https://jira.hyperledger.org/browse/FAB-14369)Fix dev mode failing to build Chaincode +* [420ba23](https://github.com/hyperledger/fabric-samples/commit/420ba23) [FAB-12762](https://jira.hyperledger.org/browse/FAB-12762) Add etcd/raft consensus option to BYFN +* [2b68c80](https://github.com/hyperledger/fabric-samples/commit/2b68c80) [FAB-14317](https://jira.hyperledger.org/browse/FAB-14317) Add default policies to org3 +* [f942010](https://github.com/hyperledger/fabric-samples/commit/f942010) [FAB-14268](https://jira.hyperledger.org/browse/FAB-14268) Make BYFN/EYFN ports match external ports +* [4e2ce23](https://github.com/hyperledger/fabric-samples/commit/4e2ce23) [FAB-14271](https://jira.hyperledger.org/browse/FAB-14271) Add channel policies to channel config +* [f26477c](https://github.com/hyperledger/fabric-samples/commit/f26477c) [FAB-11796](https://jira.hyperledger.org/browse/FAB-11796)high-throughput:Remove unnecessary prunesafe +* [137327a](https://github.com/hyperledger/fabric-samples/commit/137327a) [FAB-14162](https://jira.hyperledger.org/browse/FAB-14162) Pin fabric-samples in master to "unstable" +* [6007c09](https://github.com/hyperledger/fabric-samples/commit/6007c09) [FAB-13862](https://jira.hyperledger.org/browse/FAB-13862) Rename example02 ABstore +* [94cb603](https://github.com/hyperledger/fabric-samples/commit/94cb603) [FAB-13933](https://jira.hyperledger.org/browse/FAB-13933) Fix misspellings +* [a8a5539](https://github.com/hyperledger/fabric-samples/commit/a8a5539) Fix doc link Fix variable error +* [b0cda61](https://github.com/hyperledger/fabric-samples/commit/b0cda61) [FAB-13769](https://jira.hyperledger.org/browse/FAB-13769) Add UT code to ABAC sample Chaincode +* [c7438e1](https://github.com/hyperledger/fabric-samples/commit/c7438e1) [FAB-13668](https://jira.hyperledger.org/browse/FAB-13668) BYFN's container volume mapping is bad +* [e48b2de](https://github.com/hyperledger/fabric-samples/commit/e48b2de) [FAB-13489](https://jira.hyperledger.org/browse/FAB-13489) fabric-samples add error msg +* [8a458b5](https://github.com/hyperledger/fabric-samples/commit/8a458b5) [FAB-12056](https://jira.hyperledger.org/browse/FAB-12056) Private marbles cc use transient data +* [6269941](https://github.com/hyperledger/fabric-samples/commit/6269941) Correct broken link +* [461b6ab](https://github.com/hyperledger/fabric-samples/commit/461b6ab) FABC-781 Remove fabric-ca sample +* [e9b9477](https://github.com/hyperledger/fabric-samples/commit/e9b9477) [FAB-13372](https://jira.hyperledger.org/browse/FAB-13372) Fabric-Samples return error msg +* [e3da220](https://github.com/hyperledger/fabric-samples/commit/e3da220) [FAB-13433](https://jira.hyperledger.org/browse/FAB-13433) - Update Jenkinsfile configuration +* [33db64e](https://github.com/hyperledger/fabric-samples/commit/33db64e) Configure Stale ProBot +* [5cd277f](https://github.com/hyperledger/fabric-samples/commit/5cd277f) [FAB-11951](https://jira.hyperledger.org/browse/FAB-11951) Interest-rate swap example for SBE +* [9567985](https://github.com/hyperledger/fabric-samples/commit/9567985) [FAB-13407](https://jira.hyperledger.org/browse/FAB-13407) Align fabric-samples with 1.4.0-rc2 release +* [c7572aa](https://github.com/hyperledger/fabric-samples/commit/c7572aa) [FAB-13305](https://jira.hyperledger.org/browse/FAB-13305) Update scripts to pull latest artifacts +* [ab46e35](https://github.com/hyperledger/fabric-samples/commit/ab46e35) [FAB-13283](https://jira.hyperledger.org/browse/FAB-13283) Update sample code for commercial paper +* [f677821](https://github.com/hyperledger/fabric-samples/commit/f677821) [FAB-13232](https://jira.hyperledger.org/browse/FAB-13232) fix peer node start command +* [6a7472e](https://github.com/hyperledger/fabric-samples/commit/6a7472e) [FAB-13126](https://jira.hyperledger.org/browse/FAB-13126) Align fabric-samples with 1.4.0-rc1 release +* [445ccbc](https://github.com/hyperledger/fabric-samples/commit/445ccbc) [FAB-12880](https://jira.hyperledger.org/browse/FAB-12880) Move old prog model samples for FabCar +* [5be62b5](https://github.com/hyperledger/fabric-samples/commit/5be62b5) [FAB-13207](https://jira.hyperledger.org/browse/FAB-13207) Remove incorrect discovery options +* [fdbd92d](https://github.com/hyperledger/fabric-samples/commit/fdbd92d) [FAB-13206](https://jira.hyperledger.org/browse/FAB-13206) Remove dependencies on fabric-client +* [eff0046](https://github.com/hyperledger/fabric-samples/commit/eff0046) [FAB-12877](https://jira.hyperledger.org/browse/FAB-12877) Add fabcar app using new prog model (JS) +* [c184196](https://github.com/hyperledger/fabric-samples/commit/c184196) [FAB-12878](https://jira.hyperledger.org/browse/FAB-12878) Add fabcar app using new prog model (TS) +* [e1a39e6](https://github.com/hyperledger/fabric-samples/commit/e1a39e6) [FAB-12724](https://jira.hyperledger.org/browse/FAB-12724) Upgrade from 1.3.x to 1.4.0 +* [c21bbba](https://github.com/hyperledger/fabric-samples/commit/c21bbba) Update samples to use new logging env variables +* [7ad9f19](https://github.com/hyperledger/fabric-samples/commit/7ad9f19) [FAB-13011](https://jira.hyperledger.org/browse/FAB-13011) add kafka consensus type to byfn sample +* [33f064f](https://github.com/hyperledger/fabric-samples/commit/33f064f) [FAB-13170](https://jira.hyperledger.org/browse/FAB-13170) Add memberOnlyRead to marbles sample +* [928b72b](https://github.com/hyperledger/fabric-samples/commit/928b72b) [FAB-12875](https://jira.hyperledger.org/browse/FAB-12875) Add automated tests for fabcar sample +* [5c087f1](https://github.com/hyperledger/fabric-samples/commit/5c087f1) [FAB-13046](https://jira.hyperledger.org/browse/FAB-13046) Update TypeScript contract dependencies +* [3748983](https://github.com/hyperledger/fabric-samples/commit/3748983) [FAB-12879](https://jira.hyperledger.org/browse/FAB-12879) Update fabcar script for new contracts +* [4fb3b57](https://github.com/hyperledger/fabric-samples/commit/4fb3b57) [FAB-12852](https://jira.hyperledger.org/browse/FAB-12852) Add fabcar contract w/ new prog model (TS) +* [9facb42](https://github.com/hyperledger/fabric-samples/commit/9facb42) [FAB-12851](https://jira.hyperledger.org/browse/FAB-12851) Add fabcar contract w/ new prog model (JS) +* [e67fcf1](https://github.com/hyperledger/fabric-samples/commit/e67fcf1) [FAB-12322](https://jira.hyperledger.org/browse/FAB-12322) Update commercial-paper sample +* [fd6e2c4](https://github.com/hyperledger/fabric-samples/commit/fd6e2c4) [FAB-12703](https://jira.hyperledger.org/browse/FAB-12703) Fix misspelling "lauches" +* [c05f172](https://github.com/hyperledger/fabric-samples/commit/c05f172) [FAB-12608](https://jira.hyperledger.org/browse/FAB-12608) Update pipeline script +* [286861e](https://github.com/hyperledger/fabric-samples/commit/286861e) [FAB-12371](https://jira.hyperledger.org/browse/FAB-12371)Fix the abac sample to use new cid package +* [24c5e47](https://github.com/hyperledger/fabric-samples/commit/24c5e47) [FAB-12026](https://jira.hyperledger.org/browse/FAB-12026) pagination samples for node marbles02 +* [6dc5ce5](https://github.com/hyperledger/fabric-samples/commit/6dc5ce5) [FAB-12587](https://jira.hyperledger.org/browse/FAB-12587) Fix for Query Block by block hash API +* [df311ce](https://github.com/hyperledger/fabric-samples/commit/df311ce) [FAB-12173](https://jira.hyperledger.org/browse/FAB-12173) balance-transfer: Update anchor peers +* [c925148](https://github.com/hyperledger/fabric-samples/commit/c925148) [FAB-12415](https://jira.hyperledger.org/browse/FAB-12415) samples for 1.3.0 (master cleanup) +* [3a12c60](https://github.com/hyperledger/fabric-samples/commit/3a12c60) [FAB-12275](https://jira.hyperledger.org/browse/FAB-12275) Fix the warn in creating genesis block +* [c6f6324](https://github.com/hyperledger/fabric-samples/commit/c6f6324) [FAB-12257](https://jira.hyperledger.org/browse/FAB-12257) allow balance-transfer for doscovery +* [4445e8d](https://github.com/hyperledger/fabric-samples/commit/4445e8d) [FAB-12272](https://jira.hyperledger.org/browse/FAB-12272) Increase MAX_RETRY to 10 +* [33d333f](https://github.com/hyperledger/fabric-samples/commit/33d333f) [FAB-12190](https://jira.hyperledger.org/browse/FAB-12190) Update stable version in CI scripts +* [edee638](https://github.com/hyperledger/fabric-samples/commit/edee638) [FAB-12184](https://jira.hyperledger.org/browse/FAB-12184) Prepare fabric-samples for 1.3.0-rc1 +* [514d456](https://github.com/hyperledger/fabric-samples/commit/514d456) [FAB-12170](https://jira.hyperledger.org/browse/FAB-12170) Fix dependency check in java chaincode +* [9f80e47](https://github.com/hyperledger/fabric-samples/commit/9f80e47) [FAB-12119](https://jira.hyperledger.org/browse/FAB-12119) Fix groupId in java chaincodes +* [3237229](https://github.com/hyperledger/fabric-samples/commit/3237229) [FAB-12073](https://jira.hyperledger.org/browse/FAB-12073) Fix Org3 peers CouchDB config. +* [eece3d8](https://github.com/hyperledger/fabric-samples/commit/eece3d8) [FAB-12106](https://jira.hyperledger.org/browse/FAB-12106) Update fabric-ca build scripts +* [f62952f](https://github.com/hyperledger/fabric-samples/commit/f62952f) [FABC-131] Change fabric-ca sample to build images +* [4089786](https://github.com/hyperledger/fabric-samples/commit/4089786) [FAB-11867](https://jira.hyperledger.org/browse/FAB-11867) Develop Apps:Sample pt 2 -- application +* [9ee57c6](https://github.com/hyperledger/fabric-samples/commit/9ee57c6) [FAB-11778](https://jira.hyperledger.org/browse/FAB-11778) Upgrade to v1.3.x from v1.2.x in byfn +* [e7a1b76](https://github.com/hyperledger/fabric-samples/commit/e7a1b76) [FAB-9386](https://jira.hyperledger.org/browse/FAB-9386) Remove Marbles sample reference to Fauxton +* [9c6acee](https://github.com/hyperledger/fabric-samples/commit/9c6acee) [FAB-12022](https://jira.hyperledger.org/browse/FAB-12022) Fix CI by increasing couchdb timeout +* [4030ebd](https://github.com/hyperledger/fabric-samples/commit/4030ebd) [FAB-11397](https://jira.hyperledger.org/browse/FAB-11397) Adding java cc +* [d776651](https://github.com/hyperledger/fabric-samples/commit/d776651) [FAB-11723](https://jira.hyperledger.org/browse/FAB-11723) Developing Apps: Sample pt 1 -- contract +* [cbbbc78](https://github.com/hyperledger/fabric-samples/commit/cbbbc78) [FAB-11577](https://jira.hyperledger.org/browse/FAB-11577) Fix balance transfer to install Chaincode +* [bfdc0b6](https://github.com/hyperledger/fabric-samples/commit/bfdc0b6) [FAB-11518](https://jira.hyperledger.org/browse/FAB-11518) +* [5930dfc](https://github.com/hyperledger/fabric-samples/commit/5930dfc) [FAB-11488](https://jira.hyperledger.org/browse/FAB-11488) Update CI script +* [ca6959c](https://github.com/hyperledger/fabric-samples/commit/ca6959c) [FAB-11311](https://jira.hyperledger.org/browse/FAB-11311) Update fabric image version +* [0ca9e6e](https://github.com/hyperledger/fabric-samples/commit/0ca9e6e) FABN-833 Update Jenkinsfile to capture build artifacts +* [a4a15cb](https://github.com/hyperledger/fabric-samples/commit/a4a15cb) [FAB-11220](https://jira.hyperledger.org/browse/FAB-11220) Samples - remove EventHub +* [c4bdc68](https://github.com/hyperledger/fabric-samples/commit/c4bdc68) [FAB-8479](https://jira.hyperledger.org/browse/FAB-8479) Added Endorsement policy +* [6edd320](https://github.com/hyperledger/fabric-samples/commit/6edd320) [FAB-9297](https://jira.hyperledger.org/browse/FAB-9297) fix README links and update bootstrap.sh +* [75e2931](https://github.com/hyperledger/fabric-samples/commit/75e2931) [FAB-10811](https://jira.hyperledger.org/browse/FAB-10811) fabric-ca sample is broken on v1.2 +* [ad40e29](https://github.com/hyperledger/fabric-samples/commit/ad40e29) [FAB-10732](https://jira.hyperledger.org/browse/FAB-10732) BYFN upgrade to v1.2 +* [20ad472](https://github.com/hyperledger/fabric-samples/commit/20ad472) [FAB-10801](https://jira.hyperledger.org/browse/FAB-10801) format and styling for shell artifacts +* [e95210e](https://github.com/hyperledger/fabric-samples/commit/e95210e) [FAB-10074](https://jira.hyperledger.org/browse/FAB-10074) CI Script for pipeline project type +* [5956178](https://github.com/hyperledger/fabric-samples/commit/5956178) [FAB-6600](https://jira.hyperledger.org/browse/FAB-6600) Sample chaincode for private data +* [21444ab](https://github.com/hyperledger/fabric-samples/commit/21444ab) [FAB-10297](https://jira.hyperledger.org/browse/FAB-10297) replace fabric-preload.sh +* [3c32c52](https://github.com/hyperledger/fabric-samples/commit/3c32c52) [FAB-10346](https://jira.hyperledger.org/browse/FAB-10346) Ensure peers are in sync in eyfn +* [2f30504](https://github.com/hyperledger/fabric-samples/commit/2f30504) [FAB-10340](https://jira.hyperledger.org/browse/FAB-10340) Fix broken link in chaincode-docker-devmode +* [a603655](https://github.com/hyperledger/fabric-samples/commit/a603655) [FAB-10306](https://jira.hyperledger.org/browse/FAB-10306) Fix config in balance-transfer +* [1cd059e](https://github.com/hyperledger/fabric-samples/commit/1cd059e) [FAB-8612](https://jira.hyperledger.org/browse/FAB-8612) Cleanup eyfn/byfn with --remove-orphans +* [3031a8c](https://github.com/hyperledger/fabric-samples/commit/3031a8c) [FAB-10235](https://jira.hyperledger.org/browse/FAB-10235) Update BYFN to use V1_2 capability +* [5cf1944](https://github.com/hyperledger/fabric-samples/commit/5cf1944) [FAB-10185](https://jira.hyperledger.org/browse/FAB-10185) Reorder for logical consistency +* [e2f4f20](https://github.com/hyperledger/fabric-samples/commit/e2f4f20) [FAB-10176](https://jira.hyperledger.org/browse/FAB-10176) Fix invalid configtx.yaml documents +* [cad2b98](https://github.com/hyperledger/fabric-samples/commit/cad2b98) [FAB-10073](https://jira.hyperledger.org/browse/FAB-10073) Fix broken link in samples/fabric-ca/README +* [ace2e5a](https://github.com/hyperledger/fabric-samples/commit/ace2e5a) [FAB-10021](https://jira.hyperledger.org/browse/FAB-10021) Typo in balance-transfer/testAPIs.sh +* [fa1d899](https://github.com/hyperledger/fabric-samples/commit/fa1d899) [FAB-9893](https://jira.hyperledger.org/browse/FAB-9893) Add .gitreview file for quicker Gerrit setup +* [98561e0](https://github.com/hyperledger/fabric-samples/commit/98561e0) [FAB-9552](https://jira.hyperledger.org/browse/FAB-9552) Fix access of an un-declared variable +* [b5dad8c](https://github.com/hyperledger/fabric-samples/commit/b5dad8c) [FAB-9317](https://jira.hyperledger.org/browse/FAB-9317) Update first-network with multi endorse +* [146f7bc](https://github.com/hyperledger/fabric-samples/commit/146f7bc) [FAB-9572](https://jira.hyperledger.org/browse/FAB-9572) Fix README to match new version of BYFN +* [010fcb5](https://github.com/hyperledger/fabric-samples/commit/010fcb5) [FAB-9326](https://jira.hyperledger.org/browse/FAB-9326) Clean up byfn.sh, drop "-m" option +* [2d6386c](https://github.com/hyperledger/fabric-samples/commit/2d6386c) [FAB-8245](https://jira.hyperledger.org/browse/FAB-8245) change first network according to the fix +* [3a5108f](https://github.com/hyperledger/fabric-samples/commit/3a5108f) [FAB-9475](https://jira.hyperledger.org/browse/FAB-9475) Fix a dead link in README +* [77a6568](https://github.com/hyperledger/fabric-samples/commit/77a6568) [FAB-9294](https://jira.hyperledger.org/browse/FAB-9294) eliminate excess noise in BYFN +* [41f5ab8](https://github.com/hyperledger/fabric-samples/commit/41f5ab8) [FAB-9406](https://jira.hyperledger.org/browse/FAB-9406) Typo in byfn.sh +* [f5c2eb8](https://github.com/hyperledger/fabric-samples/commit/f5c2eb8) [FAB-9362](https://jira.hyperledger.org/browse/FAB-9362) add CONTRIBUTING.md and CODE_OF_CONDUCT.md +* [9d518fb](https://github.com/hyperledger/fabric-samples/commit/9d518fb) [FAB-9330](https://jira.hyperledger.org/browse/FAB-9330) Refactor top-level .gitignore into subdirs +* [a080da3](https://github.com/hyperledger/fabric-samples/commit/a080da3) [FAB-8958](https://jira.hyperledger.org/browse/FAB-8958) Add org3 removal to byfn.sh +* [8fc0865](https://github.com/hyperledger/fabric-samples/commit/8fc0865) [FAB-9185](https://jira.hyperledger.org/browse/FAB-9185) add /config to .gitignore +* [680ff01](https://github.com/hyperledger/fabric-samples/commit/680ff01) [FAB-8600](https://jira.hyperledger.org/browse/FAB-8600)Clear hyperledger-related containers only +* [4f97717](https://github.com/hyperledger/fabric-samples/commit/4f97717) [FAB-8947](https://jira.hyperledger.org/browse/FAB-8947) Fabric-Samples remove package lock +* [fcf62ad](https://github.com/hyperledger/fabric-samples/commit/fcf62ad) [FAB-8265](https://jira.hyperledger.org/browse/FAB-8265) Fixed spelling error in registerUser +* [e4d7760](https://github.com/hyperledger/fabric-samples/commit/e4d7760) [ [FAB-8730](https://jira.hyperledger.org/browse/FAB-8730) ] hyphen breaks fabric-samples +* [2bbb0a8](https://github.com/hyperledger/fabric-samples/commit/2bbb0a8) [FAB-8630](https://jira.hyperledger.org/browse/FAB-8630) byfn failing intermittently in CI +* [823fb6b](https://github.com/hyperledger/fabric-samples/commit/823fb6b) [ [FAB-8679](https://jira.hyperledger.org/browse/FAB-8679) ] Permit samples to use RootCAs +* [9f9fc7e](https://github.com/hyperledger/fabric-samples/commit/9f9fc7e) [FAB-8633](https://jira.hyperledger.org/browse/FAB-8633) Correct revoked error check +* [f3b55c9](https://github.com/hyperledger/fabric-samples/commit/f3b55c9) [FAB-8621](https://jira.hyperledger.org/browse/FAB-8621) Remove Marbles index json data wrapper +* [f110a6e](https://github.com/hyperledger/fabric-samples/commit/f110a6e) [FAB-8602](https://jira.hyperledger.org/browse/FAB-8602) Add volumes to first-network e2e yaml +* [7362928](https://github.com/hyperledger/fabric-samples/commit/7362928) [FAB-8567](https://jira.hyperledger.org/browse/FAB-8567) Alt: Always use volumes for ledger (m) +* [afb3d62](https://github.com/hyperledger/fabric-samples/commit/afb3d62) [FAB-8561](https://jira.hyperledger.org/browse/FAB-8561) Add note to readthedocs link in README +* [10526d5](https://github.com/hyperledger/fabric-samples/commit/10526d5) [FAB-8564](https://jira.hyperledger.org/browse/FAB-8564) add debug commands to byfn +* [e73a481](https://github.com/hyperledger/fabric-samples/commit/e73a481) [FAB-8568](https://jira.hyperledger.org/browse/FAB-8568) BYFN: Fix IMAGE_TAG for couchdb +* [ffd7a25](https://github.com/hyperledger/fabric-samples/commit/ffd7a25) [FAB-6400](https://jira.hyperledger.org/browse/FAB-6400) Balance-transfer filtered events +* [cba57da](https://github.com/hyperledger/fabric-samples/commit/cba57da) [FAB-8165](https://jira.hyperledger.org/browse/FAB-8165) Adding upgrade function to byfn +* [c6166d6](https://github.com/hyperledger/fabric-samples/commit/c6166d6) [FAB-8540](https://jira.hyperledger.org/browse/FAB-8540) Add ledger persistance to first-network +* [77e74b7](https://github.com/hyperledger/fabric-samples/commit/77e74b7) [FAB-8497](https://jira.hyperledger.org/browse/FAB-8497) Download images required for fabric-ca +* [2bed1ef](https://github.com/hyperledger/fabric-samples/commit/2bed1ef) [FAB-8539](https://jira.hyperledger.org/browse/FAB-8539) Add version checking to first-network +* [7b7fc09](https://github.com/hyperledger/fabric-samples/commit/7b7fc09) [FAB-8496](https://jira.hyperledger.org/browse/FAB-8496) allow modification of affiliations +* [981efba](https://github.com/hyperledger/fabric-samples/commit/981efba) [FAB-8503](https://jira.hyperledger.org/browse/FAB-8503) Prevent CLI container from exiting +* [c93268f](https://github.com/hyperledger/fabric-samples/commit/c93268f) [FAB-8518](https://jira.hyperledger.org/browse/FAB-8518) Not all compose files have IMAGE_TAG +* [4f2cd8d](https://github.com/hyperledger/fabric-samples/commit/4f2cd8d) [FAB-8445](https://jira.hyperledger.org/browse/FAB-8445) Adding IMAGE_TAG option to byfn +* [ca80163](https://github.com/hyperledger/fabric-samples/commit/ca80163) [FAB-8387](https://jira.hyperledger.org/browse/FAB-8387) Add gencrl to revoke command +* [e379ac5](https://github.com/hyperledger/fabric-samples/commit/e379ac5) [FAB-8407](https://jira.hyperledger.org/browse/FAB-8407) Fix TLS bad cert error +* [02ca1dc](https://github.com/hyperledger/fabric-samples/commit/02ca1dc) [FAB-7494](https://jira.hyperledger.org/browse/FAB-7494) Fix bootstrap peer config +* [41e144f](https://github.com/hyperledger/fabric-samples/commit/41e144f) [FAB-8386](https://jira.hyperledger.org/browse/FAB-8386) eyfn fails to execute invoke on org3 +* [4ab098f](https://github.com/hyperledger/fabric-samples/commit/4ab098f) [FAB-8327](https://jira.hyperledger.org/browse/FAB-8327) Change eyfn.sh to use configtxlator cli +* [24f35c1](https://github.com/hyperledger/fabric-samples/commit/24f35c1) [FAB-7750](https://jira.hyperledger.org/browse/FAB-7750) first network with support to [FAB-5664](https://jira.hyperledger.org/browse/FAB-5664) +* [305d6f4](https://github.com/hyperledger/fabric-samples/commit/305d6f4) [FAB-8238](https://jira.hyperledger.org/browse/FAB-8238)wrong orderer/peer type in fabric-ca sam +* [1d69e9e](https://github.com/hyperledger/fabric-samples/commit/1d69e9e) [FAB-7540](https://jira.hyperledger.org/browse/FAB-7540) Simplifies Reconfigure Your Network tutorial +* [0daa8bc](https://github.com/hyperledger/fabric-samples/commit/0daa8bc) [FAB-8122](https://jira.hyperledger.org/browse/FAB-8122) Updated README.md file- balancetransfer +* [652f074](https://github.com/hyperledger/fabric-samples/commit/652f074) [FAB-7342](https://jira.hyperledger.org/browse/FAB-7342) Enable client auth in fabric-ca sample +* [90c2bfd](https://github.com/hyperledger/fabric-samples/commit/90c2bfd) [FAB-6934](https://jira.hyperledger.org/browse/FAB-6934) fix devmode sample for v1.1 +* [2c0c2c7](https://github.com/hyperledger/fabric-samples/commit/2c0c2c7) [FAB-7910](https://jira.hyperledger.org/browse/FAB-7910) Clean up chaincode containers in fabric-ca +* [bbee1b2](https://github.com/hyperledger/fabric-samples/commit/bbee1b2) [FAB-7908](https://jira.hyperledger.org/browse/FAB-7908) Change hf.admin attr to admin +* [dd12f88](https://github.com/hyperledger/fabric-samples/commit/dd12f88) [FAB-7834](https://jira.hyperledger.org/browse/FAB-7834) Add couchdb index to marbles02 sample +* [25f6091](https://github.com/hyperledger/fabric-samples/commit/25f6091) [FAB-7836](https://jira.hyperledger.org/browse/FAB-7836) Fix "No identity type provided" Error +* [5eb2fb2](https://github.com/hyperledger/fabric-samples/commit/5eb2fb2) [FAB-7653](https://jira.hyperledger.org/browse/FAB-7653) Fix incorrect chaincode location +* [5a974a4](https://github.com/hyperledger/fabric-samples/commit/5a974a4) [FAB-7533](https://jira.hyperledger.org/browse/FAB-7533) Fix typo in balance-transfer sample +* [038c496](https://github.com/hyperledger/fabric-samples/commit/038c496) [FAB-7592](https://jira.hyperledger.org/browse/FAB-7592) Give hf.Registrar attrs to admins +* [cf79cd1](https://github.com/hyperledger/fabric-samples/commit/cf79cd1) [FAB-7584](https://jira.hyperledger.org/browse/FAB-7584) Removes copy of creds to keystore +* [a0c1687](https://github.com/hyperledger/fabric-samples/commit/a0c1687) [FAB-7487](https://jira.hyperledger.org/browse/FAB-7487) Fix typo in node/fabcar.js +* [1883ae9](https://github.com/hyperledger/fabric-samples/commit/1883ae9) [FAB-7527](https://jira.hyperledger.org/browse/FAB-7527) Improves BYFN +* [e848216](https://github.com/hyperledger/fabric-samples/commit/e848216) [FAB-7511](https://jira.hyperledger.org/browse/FAB-7511) clear crypto material after clearing network +* [54ffa5f](https://github.com/hyperledger/fabric-samples/commit/54ffa5f) [FAB-7241](https://jira.hyperledger.org/browse/FAB-7241) Fix chaincode-devmode +* [c446510](https://github.com/hyperledger/fabric-samples/commit/c446510) [FAB-6550](https://jira.hyperledger.org/browse/FAB-6550) Sample app written in typescript +* [69a127e](https://github.com/hyperledger/fabric-samples/commit/69a127e) [FAB-6254](https://jira.hyperledger.org/browse/FAB-6254) Fix the default CLI timeout +* [7428f64](https://github.com/hyperledger/fabric-samples/commit/7428f64) [FAB-7160](https://jira.hyperledger.org/browse/FAB-7160) Samples - readme sample commands +* [2474704](https://github.com/hyperledger/fabric-samples/commit/2474704) [FAB-6967](https://jira.hyperledger.org/browse/FAB-6967) Added steps to query by a revoked user +* [948e237](https://github.com/hyperledger/fabric-samples/commit/948e237) [FAB-5913](https://jira.hyperledger.org/browse/FAB-5913)balance-transfer:Fix query response message +* [7b76a7a](https://github.com/hyperledger/fabric-samples/commit/7b76a7a) [FAB-6632](https://jira.hyperledger.org/browse/FAB-6632) - Artifacts for BYFN reconfigure +* [24ef9da](https://github.com/hyperledger/fabric-samples/commit/24ef9da) [FAB-6902](https://jira.hyperledger.org/browse/FAB-6902) [FAB-6904](https://jira.hyperledger.org/browse/FAB-6904) correct the sample for FAB-6904 +* [fd795d2](https://github.com/hyperledger/fabric-samples/commit/fd795d2) [FAB-6745](https://jira.hyperledger.org/browse/FAB-6745) Fix timing issue in sample +* [a7be462](https://github.com/hyperledger/fabric-samples/commit/a7be462) [FAB-6895](https://jira.hyperledger.org/browse/FAB-6895) chaincode mounting issue +* [7c23985](https://github.com/hyperledger/fabric-samples/commit/7c23985) [FAB-5221](https://jira.hyperledger.org/browse/FAB-5221) generateCerts should delete crypto +* [1961835](https://github.com/hyperledger/fabric-samples/commit/1961835) [FAB-6870](https://jira.hyperledger.org/browse/FAB-6870) Update node modules versions +* [bb3ac84](https://github.com/hyperledger/fabric-samples/commit/bb3ac84) [FAB-5363](https://jira.hyperledger.org/browse/FAB-5363) fabric-samples update balance +* [6b2799e](https://github.com/hyperledger/fabric-samples/commit/6b2799e) [FAB-6568](https://jira.hyperledger.org/browse/FAB-6568) Fabric-Samples - update fabcar +* [fafae55](https://github.com/hyperledger/fabric-samples/commit/fafae55) [FAB-6779](https://jira.hyperledger.org/browse/FAB-6779) Fix the error in fabric-ca +* [caf5c33](https://github.com/hyperledger/fabric-samples/commit/caf5c33) [FAB-6050](https://jira.hyperledger.org/browse/FAB-6050) Adding fabric-ca sample +* [44c204d](https://github.com/hyperledger/fabric-samples/commit/44c204d) [FAB-5898](https://jira.hyperledger.org/browse/FAB-5898) porting samples to node.js chaincode +* [07a07d5](https://github.com/hyperledger/fabric-samples/commit/07a07d5) [FAB-6361](https://jira.hyperledger.org/browse/FAB-6361) Update license text in README +* [c9b0c62](https://github.com/hyperledger/fabric-samples/commit/c9b0c62) [FAB-6292](https://jira.hyperledger.org/browse/FAB-6292) Fix spelling error +* [9d70f31](https://github.com/hyperledger/fabric-samples/commit/9d70f31) Add high-throughput example to samples +* [194b9b9](https://github.com/hyperledger/fabric-samples/commit/194b9b9) [FAB-5618](https://jira.hyperledger.org/browse/FAB-5618) Allow directory to contain spaces +* [77b4090](https://github.com/hyperledger/fabric-samples/commit/77b4090) [FAB-5992](https://jira.hyperledger.org/browse/FAB-5992) Fix error in first-network dir + +## "v1.0.2" + +* [ba0a098](https://github.com/hyperledger/fabric-samples/commit/ba0a098) [FAB-5995](https://jira.hyperledger.org/browse/FAB-5995) Update samples to work with v1.0.2 +* [7cca09f](https://github.com/hyperledger/fabric-samples/commit/7cca09f) [FAB-5759](https://jira.hyperledger.org/browse/FAB-5759) fix byfn e2e test failures +* [7f1c2f4](https://github.com/hyperledger/fabric-samples/commit/7f1c2f4) [FAB-5056](https://jira.hyperledger.org/browse/FAB-5056) enable couchdb test in byfn e2e script +* [2571c96](https://github.com/hyperledger/fabric-samples/commit/2571c96) [FAB-56431](https://jira.hyperledger.org/browse/FAB-56431): Add dependency for couchdb into peer. +* [be773d2](https://github.com/hyperledger/fabric-samples/commit/be773d2) [FAB-5603](https://jira.hyperledger.org/browse/FAB-5603) fixed missing f option on switch +* [5921140](https://github.com/hyperledger/fabric-samples/commit/5921140) [FAB-5576](https://jira.hyperledger.org/browse/FAB-5576) -f flag to choose docker-compose on byfn.sh +* [79cb041](https://github.com/hyperledger/fabric-samples/commit/79cb041) [FAB-5303](https://jira.hyperledger.org/browse/FAB-5303) Further balance-transfer code optimization +* [92348cb](https://github.com/hyperledger/fabric-samples/commit/92348cb) [FAB-5453](https://jira.hyperledger.org/browse/FAB-5453) Enable CouchDB passwords in Fabric Samples +* [2f4e9b8](https://github.com/hyperledger/fabric-samples/commit/2f4e9b8) [FAB-5130](https://jira.hyperledger.org/browse/FAB-5130) Invalid syntax in docker compose files +* [7778837](https://github.com/hyperledger/fabric-samples/commit/7778837) [FAB-5394](https://jira.hyperledger.org/browse/FAB-5394) Introduce Delay as configurable variable +* [134549a](https://github.com/hyperledger/fabric-samples/commit/134549a) [FAB-5412](https://jira.hyperledger.org/browse/FAB-5412) add fabric-preload.sh script +* [a7e83fc](https://github.com/hyperledger/fabric-samples/commit/a7e83fc) [FAB-5392](https://jira.hyperledger.org/browse/FAB-5392) Fix path issue for Git Bash users +* [6cce07c](https://github.com/hyperledger/fabric-samples/commit/6cce07c) [FAB-5155](https://jira.hyperledger.org/browse/FAB-5155)Fix README.md in "balance-transfer" example +* [6422fc3](https://github.com/hyperledger/fabric-samples/commit/6422fc3) [FAB-5311](https://jira.hyperledger.org/browse/FAB-5311) Fix spelling error +* [e0db341](https://github.com/hyperledger/fabric-samples/commit/e0db341) [FAB-5287](https://jira.hyperledger.org/browse/FAB-5287) NodeSDK - fix sample +* [ca8fad3](https://github.com/hyperledger/fabric-samples/commit/ca8fad3) [FAB-5260](https://jira.hyperledger.org/browse/FAB-5260) Update samples to v1.0.0 +* [8f01c7f](https://github.com/hyperledger/fabric-samples/commit/8f01c7f) [FAB-5260](https://jira.hyperledger.org/browse/FAB-5260) Update balance-transfer sample to v1.0.0 +* [6899719](https://github.com/hyperledger/fabric-samples/commit/6899719) [FAB-5195](https://jira.hyperledger.org/browse/FAB-5195) byfn.sh help text is incorrect +* [70bff28](https://github.com/hyperledger/fabric-samples/commit/70bff28) [FAB-5197](https://jira.hyperledger.org/browse/FAB-5197)Check all prop-responses in balance-transfer +* [f9c2954](https://github.com/hyperledger/fabric-samples/commit/f9c2954) [FAB-5038](https://jira.hyperledger.org/browse/FAB-5038) Generate artifacts if they don't exist +* [957a1ff](https://github.com/hyperledger/fabric-samples/commit/957a1ff) [FAB-5062](https://jira.hyperledger.org/browse/FAB-5062) Correct Gossip bootstrap setup in byfn +* [d58af56](https://github.com/hyperledger/fabric-samples/commit/d58af56) [FAB-5031](https://jira.hyperledger.org/browse/FAB-5031) Fix e2e template typo +* [04c9eff](https://github.com/hyperledger/fabric-samples/commit/04c9eff) [FAB-4996](https://jira.hyperledger.org/browse/FAB-4996) NodeSDK - move sample to fabric-samples +* [6610584](https://github.com/hyperledger/fabric-samples/commit/6610584) [FAB-4994](https://jira.hyperledger.org/browse/FAB-4994) Fix formatting of js files +* [e265cac](https://github.com/hyperledger/fabric-samples/commit/e265cac) [FAB-4988](https://jira.hyperledger.org/browse/FAB-4988) Update certs generated using rc1 binary +* [ba777f3](https://github.com/hyperledger/fabric-samples/commit/ba777f3) [FAB-4371](https://jira.hyperledger.org/browse/FAB-4371) - Chaincode Dev Mode +* [ca0b5d2](https://github.com/hyperledger/fabric-samples/commit/ca0b5d2) [FAB-4916](https://jira.hyperledger.org/browse/FAB-4916) add marbles02 chaincode +* [b4beeff](https://github.com/hyperledger/fabric-samples/commit/b4beeff) [FAB-4927](https://jira.hyperledger.org/browse/FAB-4927) Modify TLS config +* [803da71](https://github.com/hyperledger/fabric-samples/commit/803da71) [FAB-4430](https://jira.hyperledger.org/browse/FAB-4430) - WYFA scripts and node code +* [241d08e](https://github.com/hyperledger/fabric-samples/commit/241d08e) [FAB-4910](https://jira.hyperledger.org/browse/FAB-4910) fix incorrect network spec +* [fbc120f](https://github.com/hyperledger/fabric-samples/commit/fbc120f) [FAB-4851](https://jira.hyperledger.org/browse/FAB-4851) basic network sample +* [3297865](https://github.com/hyperledger/fabric-samples/commit/3297865) [FAB-4073](https://jira.hyperledger.org/browse/FAB-4073) build your first network sample +* [54cae68](https://github.com/hyperledger/fabric-samples/commit/54cae68) [FAB-4853](https://jira.hyperledger.org/browse/FAB-4853) initial content +* [abb2b38](https://github.com/hyperledger/fabric-samples/commit/abb2b38) Initial empty repository diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..8fcbd09 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Fabric Samples Maintaners +* @hyperledger/fabric-samples-maintainers diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2f1f0e3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,8 @@ +Code of Conduct Guidelines +========================== + +Please review the Hyperledger [Code of +Conduct](https://wiki.hyperledger.org/community/hyperledger-project-code-of-conduct) +before participating. It is important that we keep things civil. + +Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6ac0fc4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +## Contributing + +We welcome contributions to the Hyperledger Fabric Project in many forms, and +there's always plenty to do! + +Please visit the +[contributors guide](http://hyperledger-fabric.readthedocs.io/en/latest/CONTRIBUTING.html) in the +docs to learn how to make contributions to this exciting project. + +## Code of Conduct Guidelines + +See our [Code of Conduct Guidelines](./CODE_OF_CONDUCT.md). + +## Maintainers + +Should you have any questions or concerns, please reach out to one of the project's [Maintainers](./MAINTAINERS.md). + +Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..a7dc8aa --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,20 @@ +Maintainers +=========== + +fabric-samples uses a non-author code review policy, requiring a single approval from a non-author maintainer. + +| Name | GitHub | Chat | email | +|---------------------------|------------------|----------------|-------------------------------------| +| Arnaud Le Hors | lehors | lehors | lehors@us.ibm.com | +| Bret Harrison | harrisob | bretharrison | harrisob@us.ibm.com | +| Chris Ferris | christo4ferris | cbf | chris.ferris@gmail.com | +| Dave Enyeart | denyeart | dave.enyeart | enyeart@us.ibm.com | +| Gari Singh | mastersingh24 | mastersingh24 | gari.r.singh@gmail.com | +| Jason Yellick | jyellick | jyellick | jyellick@us.ibm.com | +| Matthew B White | mbwhite | mbwhite | whitemat@uk.ibm.com | +| Nikhil Gupta | nikhil550 | negupta | nikhilg550@gmail.com | +| Simon Stone | sstone1 | sstone1 | sstone1@uk.ibm.com | + +Also: Please see the [Release Manager section](https://github.com/hyperledger/fabric/blob/master/MAINTAINERS.md) + +Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0edf4ae --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +[//]: # (SPDX-License-Identifier: CC-BY-4.0) + +# Hyperledger Fabric Samples + +You can use Fabric samples to get started working with Hyperledger Fabric, explore important Fabric features, and learn how to build applications that can interact with blockchain networks using the Fabric SDKs. To learn more about Hyperledger Fabric, visit the [Fabric documentation](https://hyperledger-fabric.readthedocs.io/en/latest). + +## Getting started with the Fabric samples + +To use the Fabric samples, you need to download the Fabric Docker images and the Fabric CLI tools. First, make sure that you have installed all of the [Fabric prerequisites](https://hyperledger-fabric.readthedocs.io/en/latest/prereqs.html). You can then follow the instructions to [Install the Fabric Samples, Binaries, and Docker Images](https://hyperledger-fabric.readthedocs.io/en/latest/install.html) in the Fabric documentation. In addition to downloading the Fabric images and tool binaries, the Fabric samples will also be cloned to your local machine. + +## Test network + +The [Fabric test network](test-network) in the samples repository provides a Docker Compose based test network with two +Organization peers and an ordering service node. You can use it on your local machine to run the samples listed below. +You can also use it to deploy and test your own Fabric chaincodes and applications. To get started, see +the [test network tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/test_network.html). + +## Asset transfer samples and tutorials + +The asset transfer series provides a series of sample smart contracts and applications to demonstrate how to store and transfer assets using Hyperledger Fabric. +Each sample and associated tutorial in the series demonstrates a different core capability in Hyperledger Fabric. The **Basic** sample provides an introduction on how +to write smart contracts and how to interact with a Fabric network using the Fabric SDKs. The **Ledger queries**, **Private data**, and **State-based endorsement** +samples demonstrate these additional capabilities. Finally, the **Secured agreement** sample demonstrates how to bring all the capabilities together to securely +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, 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, Java | 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 | +| [Events](asset-transfer-events) | The events sample demonstrates how smart contracts can emit events that are read by the applications interacting with the network. | [README](asset-transfer-events/README.md) | JavaScript, Java | JavaScript | +| [Attribute-based access control](asset-transfer-abac) | Demonstrates the use of attribute and identity based access control using a simple asset transfer scenario | [README](asset-transfer-abac/README.md) | Go | None | + + + +## Additional samples + +Additional samples demonstrate various Fabric use cases and application patterns. + +| **Sample** | **Description** | **Documentation** | +| -------------|------------------------------|------------------| +| [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 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) | +| [Auction](auction) | Run an auction where bids are kept private until the auction is closed, after which users can reveal their bid | [README](auction/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** | | + +## License + +Hyperledger Project source code files are made available under the Apache +License, Version 2.0 (Apache-2.0), located in the [LICENSE](LICENSE) file. +Hyperledger Project documentation files are made available under the Creative +Commons Attribution 4.0 International License (CC-BY-4.0), available at http://creativecommons.org/licenses/by/4.0/. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..91509aa --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Hyperledger Security Policy + +## Reporting a Security Bug + +If you think you have discovered a security issue in any of the Hyperledger projects, we'd love to hear from you. We will take all security bugs seriously and if confirmed upon investigation we will patch it within a reasonable amount of time and release a public security bulletin discussing the impact and credit the discoverer. + +There are two ways to report a security bug. The easiest is to email a description of the flaw and any related information (e.g. reproduction steps, version) to [security at hyperledger dot org](mailto:security@hyperledger.org). + +The other way is to file a confidential security bug in our [JIRA bug tracking system](https://jira.hyperledger.org). Be sure to set the “Security Level†to “Security issueâ€. + +The process by which the Hyperledger Security Team handles security bugs is documented further in our [Defect Response page](https://wiki.hyperledger.org/display/HYP/Defect+Response) on our [wiki](https://wiki.hyperledger.org). + diff --git a/asset-transfer-abac/README.md b/asset-transfer-abac/README.md new file mode 100644 index 0000000..46350d8 --- /dev/null +++ b/asset-transfer-abac/README.md @@ -0,0 +1,173 @@ +# Attribute based access control + +The `asset-transfer-abac` sample demonstrates the use of Attribute-based access control within the context of a simple asset transfer scenario. The sample also uses authorization based on individual client identities to allow the users that interact with the network to own assets on the blockchain ledger. + +Attribute-Based Access Control (ABAC) refers to the ability to restrict access to certain functionality within a smart contract based on the attributes within a users certificate. For example, you may want certain functions within a smart contract to be used only by application administrators. ABAC allows organizations to provide elevated access to certain users without requiring those users be administrators of the network. For more information, see [Attribute-based access control](https://hyperledger-fabric-ca.readthedocs.io/en/latest/users-guide.html#attribute-based-access-control) in the Fabric CA users guide. + +The `asset-transfer-abac` smart contract allows you to create assets that can be updated or transferred by the asset owner. However, the ability to create or remove assets from the ledger is restricted to identities with the `abac.creator=true` attribute. The identity that creates the asset is assigned as the asset owner. Only the owner can transfer the asset to a new owner or update the asset properties. In the course of the tutorial, we will use the Fabric CA client to create identities with the attribute required to create a new asset. We will then transfer the asset to another identity and demonstrate how the `GetID()` function can be used to enforce asset ownership. We will then use the owner identity to delete the asset. Both Attribute based access control and the `GetID()` function are provided by the [Client Identity Chaincode Library](https://github.com/hyperledger/fabric-chaincode-go/blob/master/pkg/cid/README.md). + +## Start the network and deploy the smart contract + +We can use the Fabric test network to deploy and interact with the `asset-transfer-abac` smart contract. Run the following command to change into the test network directory and bring down any running nodes: +``` +cd fabric-samples/test-network +./network.sh down +``` + +Run the following command to deploy the test network using Certificate Authorities: +``` +./network.sh up createChannel -ca +``` + +You can then use the test network script to deploy the `asset-transfer-abac` smart contract to a channel on the network: +``` +./network.sh deployCC -ccn abac -ccp ../asset-transfer-abac/chaincode-go/ -ccl go +``` + +## Register identities with attributes + +We can use the one of the test network Certificate Authorities to register and enroll identities with the attribute of `abac.creator=true`. First, we need to set the following environment variables in order to use the Fabric CA client. +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +``` + +We will create the identities using the Org1 CA. Set the Fabric CA client home to the MSP of the Org1 CA admin: +``` +export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/ +``` + +There are two ways to generate certificates with attributes added. We will use both methods and create two identities in the process. The first method is to specify that the attribute be added to the certificate by default when the identity is registered. The following command will register an identity named creator1 with the attribute of `abac.creator=true`. + +``` +fabric-ca-client register --id.name creator1 --id.secret creator1pw --id.type client --id.affiliation org1 --id.attrs 'abac.creator=true:ecert' --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +The `ecert` suffix adds the attribute to the certificate automatically when the identity is enrolled. As a result, the following enroll command will contain the attribute that was provided in the registration command. + +``` +fabric-ca-client enroll -u https://creator1:creator1pw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/creator1@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +Now that we have enrolled the identity, run the command below to copy the Node OU configuration file into the creator1 MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/creator1@org1.example.com/msp/config.yaml +``` + +The second method is to request that the attribute be added upon enrollment. The following command will register an identity named creator2 with the same `abac.creator` attribute. +``` +fabric-ca-client register --id.name creator2 --id.secret creator2pw --id.type client --id.affiliation org1 --id.attrs 'abac.creator=true:' --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +The following enroll command will add the attribute to the certificate: + +``` +fabric-ca-client enroll -u https://creator2:creator2pw@localhost:7054 --caname ca-org1 --enrollment.attrs "abac.creator" -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/creator2@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 creator2 MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/creator2@org1.example.com/msp/config.yaml +``` + +## Create an asset + +You can use either identity with the `abac.creator=true` attribute to create an asset using the `asset-transfer-abac` smart contract. We will set the following environment variables to use the first identity that was generated, creator1: + +``` +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/creator1@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" +``` + +Run the following command to create Asset1: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n abac -c '{"function":"CreateAsset","Args":["Asset1","blue","20","100"]}' +``` + +You can use the command below to query the asset on the ledger: +``` +peer chaincode query -C mychannel -n abac -c '{"function":"ReadAsset","Args":["Asset1"]}' +``` +The result will list the creator1 identity as the asset owner. The `GetID()` API reads the name and issuer from the certificate of the identity that submitted the transaction and assigns that identity as the asset owner: +``` +{"ID":"Asset1","color":"blue","size":20,"owner":"x509::CN=creator1,OU=client+OU=org1,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=US","appraisedValue":100} +``` + +## Transfer the asset + +As the owner of Asset1, the creator1 identity has the ability to transfer the asset to another owner. In order to transfer the asset, the owner needs to provide the name and issuer of the new owner to the `TransferAsset` function. The `asset-transfer-abac` smart contract has a `GetSubmittingClientIdentity` function that allows users to retrieve their certificate information and provide it to the asset owner out of band (we omit this step). Issue the command below to transfer Asset1 to the user1 identity from Org1 that was created when the test network was deployed: +``` +export RECIPIENT="x509::CN=user1,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=US" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n abac -c '{"function":"TransferAsset","Args":["Asset1","'"$RECIPIENT"'"]}' +``` +Query the ledger to verify that the asset has a new owner: +``` +peer chaincode query -C mychannel -n abac -c '{"function":"ReadAsset","Args":["Asset1"]}' +``` +We can see that Asset1 with is now owned by User1: +``` +{"ID":"Asset1","color":"blue","size":20,"owner":"x509::CN=user1,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=US","appraisedValue":100} +``` + +## Update the asset + +Now that the asset has been transferred, the new owner can update the asset properties. The smart contract uses the `GetID()` API to ensure that the update is being submitted by the asset owner. To demonstrate the difference between identity and attribute based access control, lets try to update the asset using the creator1 identity first: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n abac -c '{"function":"UpdateAsset","Args":["Asset1","green","20","100"]}' +``` + +Even though creator1 can create new assets, the smart contract detects that the transaction was not submitted by the identity that owns the asset, user1. The command returns the following error: +``` +Error: endorsement failure during invoke. response: status:500 message:"submitting client not authorized to update asset, does not own asset" +``` + +Run the following command to operate as the asset owner by setting the MSP path to User1: +``` +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp +``` + +We can now update the asset. Run the following command to change the asset color from blue to green. All other aspects of the asset will remain unchanged. +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n abac -c '{"function":"UpdateAsset","Args":["Asset1","green","20","100"]}' +``` +Run the query command again to verify that the asset has changed color: +``` +peer chaincode query -C mychannel -n abac -c '{"function":"ReadAsset","Args":["Asset1"]}' +``` +The result will display that Asset1 is now green: +``` +{"ID":"Asset1","color":"green","size":20,"owner":"x509::CN=user1,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=US","appraisedValue":100} +``` + +## Delete the asset + +The owner also has the ability to delete the asset. Run the following command to remove Asset1 from the ledger: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n abac -c '{"function":"DeleteAsset","Args":["Asset1"]}' +``` + +If you query the ledger once more, you will see that Asset1 no longer exists: +``` +peer chaincode query -C mychannel -n abac -c '{"function":"ReadAsset","Args":["Asset1"]}' +``` + +While we are operating as User1, we can demonstrate attribute based access control by trying to create an asset using an identity without the `abac.creator=true` attribute. Run the following command to try to create Asset1 as User1: +``` +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n abac -c '{"function":"CreateAsset","Args":["Asset2","red","20","100"]}' +``` + +The smart contract will return the following error: +``` +Error: endorsement failure during invoke. response: status:500 message:"submitting client not authorized to create asset, does not have abac.creator role" +``` + +## Clean up + +When you are finished, you can run the following command to bring down the test network: +``` +./network.sh down +``` diff --git a/asset-transfer-abac/chaincode-go/go.mod b/asset-transfer-abac/chaincode-go/go.mod new file mode 100644 index 0000000..4144e4c --- /dev/null +++ b/asset-transfer-abac/chaincode-go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/asset-transfer-abac/chaincode-go + +go 1.15 + +require github.com/hyperledger/fabric-contract-api-go v1.1.1 diff --git a/asset-transfer-abac/chaincode-go/go.sum b/asset-transfer-abac/chaincode-go/go.sum new file mode 100644 index 0000000..116a448 --- /dev/null +++ b/asset-transfer-abac/chaincode-go/go.sum @@ -0,0 +1,138 @@ +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 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.1 h1:gDhOC18gjgElNZ85kFWsbCQq95hyUP/21n++m0Sv6B0= +github.com/hyperledger/fabric-contract-api-go v1.1.1/go.mod h1:+39cWxbh5py3NtXpRA63rAH7NzXyED+QJx1EZr0tJPo= +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/asset-transfer-abac/chaincode-go/smart-contract/abac.go b/asset-transfer-abac/chaincode-go/smart-contract/abac.go new file mode 100644 index 0000000..746b09c --- /dev/null +++ b/asset-transfer-abac/chaincode-go/smart-contract/abac.go @@ -0,0 +1,214 @@ +package abac + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// SmartContract provides functions for managing an Asset +type SmartContract struct { + contractapi.Contract +} + +// Asset describes basic details of what makes up a simple asset +type Asset struct { + ID string `json:"ID"` + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` + AppraisedValue int `json:"appraisedValue"` +} + +// CreateAsset issues a new asset to the world state with given details. +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, appraisedValue int) error { + + // Demonstrate the use of Attribute-Based Access Control (ABAC) by checking + // to see if the caller has the "abac.creator" attribute with a value of true; + // if not, return an error. + // + err := ctx.GetClientIdentity().AssertAttributeValue("abac.creator", "true") + if err != nil { + return fmt.Errorf("submitting client not authorized to create asset, does not have abac.creator role") + } + + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if exists { + return fmt.Errorf("the asset %s already exists", id) + } + + // Get ID of submitting client identity + clientID, err := s.GetSubmittingClientIdentity(ctx) + if err != nil { + return err + } + + asset := Asset{ + ID: id, + Color: color, + Size: size, + Owner: clientID, + AppraisedValue: appraisedValue, + } + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// UpdateAsset updates an existing asset in the world state with provided parameters. +func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, newColor string, newSize int, newValue int) error { + + asset, err := s.ReadAsset(ctx, id) + if err != nil { + return err + } + + clientID, err := s.GetSubmittingClientIdentity(ctx) + if err != nil { + return err + } + + if clientID != asset.Owner { + return fmt.Errorf("submitting client not authorized to update asset, does not own asset") + } + + asset.Color = newColor + asset.Size = newSize + asset.AppraisedValue = newValue + + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// DeleteAsset deletes a given asset from the world state. +func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error { + + asset, err := s.ReadAsset(ctx, id) + if err != nil { + return err + } + + clientID, err := s.GetSubmittingClientIdentity(ctx) + if err != nil { + return err + } + + if clientID != asset.Owner { + return fmt.Errorf("submitting client not authorized to update asset, does not own asset") + } + + return ctx.GetStub().DelState(id) +} + +// TransferAsset updates the owner field of asset with given id in world state. +func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error { + + asset, err := s.ReadAsset(ctx, id) + if err != nil { + return err + } + + clientID, err := s.GetSubmittingClientIdentity(ctx) + if err != nil { + return err + } + + if clientID != asset.Owner { + return fmt.Errorf("submitting client not authorized to update asset, does not own asset") + } + + asset.Owner = newOwner + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// ReadAsset returns the asset stored in the world state with given id. +func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) { + + assetJSON, err := ctx.GetStub().GetState(id) + if err != nil { + return nil, fmt.Errorf("failed to read from world state: %v", err) + } + if assetJSON == nil { + return nil, fmt.Errorf("the asset %s does not exist", id) + } + + var asset Asset + err = json.Unmarshal(assetJSON, &asset) + if err != nil { + return nil, err + } + + return &asset, nil +} + +// GetAllAssets returns all assets found in world state +func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) { + // range query with empty string for startKey and endKey does an + // open-ended query of all assets in the chaincode namespace. + resultsIterator, err := ctx.GetStub().GetStateByRange("", "") + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + var assets []*Asset + for resultsIterator.HasNext() { + queryResponse, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + var asset Asset + err = json.Unmarshal(queryResponse.Value, &asset) + if err != nil { + return nil, err + } + assets = append(assets, &asset) + } + + return assets, nil +} + +// AssetExists returns true when asset with given ID exists in world state +func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) { + + assetJSON, err := ctx.GetStub().GetState(id) + if err != nil { + return false, fmt.Errorf("failed to read from world state: %v", err) + } + + return assetJSON != nil, nil +} + +// GetSubmittingClientIdentity returns the name and issuer of the identity that +// invokes the smart contract. This function base64 decodes the identity string +// before returning the value to the client or smart contract. +func (s *SmartContract) GetSubmittingClientIdentity(ctx contractapi.TransactionContextInterface) (string, error) { + + b64ID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return "", fmt.Errorf("Failed to read clientID: %v", err) + } + decodeID, err := base64.StdEncoding.DecodeString(b64ID) + if err != nil { + return "", fmt.Errorf("failed to base64 decode clientID: %v", err) + } + return string(decodeID), nil +} diff --git a/asset-transfer-abac/chaincode-go/smartContract.go b/asset-transfer-abac/chaincode-go/smartContract.go new file mode 100644 index 0000000..c54c78b --- /dev/null +++ b/asset-transfer-abac/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" + abac "github.com/hyperledger/fabric-samples/asset-transfer-abac/chaincode-go/smart-contract" +) + +func main() { + abacSmartContract, err := contractapi.NewChaincode(&abac.SmartContract{}) + if err != nil { + log.Panicf("Error creating abac chaincode: %v", err) + } + + if err := abacSmartContract.Start(); err != nil { + log.Panicf("Error starting abac chaincode: %v", err) + } +} diff --git a/asset-transfer-basic/.gitignore b/asset-transfer-basic/.gitignore new file mode 100755 index 0000000..1ddbcb2 --- /dev/null +++ b/asset-transfer-basic/.gitignore @@ -0,0 +1,2 @@ +wallet +!wallet/.gitkeep diff --git a/asset-transfer-basic/application-go/.gitignore b/asset-transfer-basic/application-go/.gitignore new file mode 100755 index 0000000..e9fec12 --- /dev/null +++ b/asset-transfer-basic/application-go/.gitignore @@ -0,0 +1,4 @@ +wallet +!wallet/.gitkeep + +keystore diff --git a/asset-transfer-basic/application-go/assetTransfer.go b/asset-transfer-basic/application-go/assetTransfer.go new file mode 100644 index 0000000..b9a100b --- /dev/null +++ b/asset-transfer-basic/application-go/assetTransfer.go @@ -0,0 +1,155 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" +) + +func main() { + log.Println("============ application-golang starts ============") + + err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true") + if err != nil { + log.Fatalf("Error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err) + } + + wallet, err := gateway.NewFileSystemWallet("wallet") + if err != nil { + log.Fatalf("Failed to create wallet: %v", err) + } + + if !wallet.Exists("appUser") { + err = populateWallet(wallet) + if err != nil { + log.Fatalf("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 { + log.Fatalf("Failed to connect to gateway: %v", err) + } + defer gw.Close() + + network, err := gw.GetNetwork("mychannel") + if err != nil { + log.Fatalf("Failed to get network: %v", err) + } + + 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 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) + } + log.Println(string(result)) + + 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 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.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.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.Println("--> Evaluate Transaction: ReadAsset, function returns 'asset1' attributes") + result, err = contract.EvaluateTransaction("ReadAsset", "asset1") + if err != nil { + log.Fatalf("Failed to evaluate transaction: %v", err) + } + log.Println(string(result)) + log.Println("============ application-golang ends ============") +} + +func populateWallet(wallet *gateway.Wallet) error { + log.Println("============ Populating wallet ============") + 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/asset-transfer-basic/application-go/go.mod b/asset-transfer-basic/application-go/go.mod new file mode 100644 index 0000000..dad8228 --- /dev/null +++ b/asset-transfer-basic/application-go/go.mod @@ -0,0 +1,5 @@ +module asset-transfer-basic + +go 1.14 + +require github.com/hyperledger/fabric-sdk-go v1.0.0-rc1 diff --git a/asset-transfer-basic/application-go/go.sum b/asset-transfer-basic/application-go/go.sum new file mode 100644 index 0000000..19c5b31 --- /dev/null +++ b/asset-transfer-basic/application-go/go.sum @@ -0,0 +1,277 @@ +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= +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.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/asset-transfer-basic/application-java/.gitattributes b/asset-transfer-basic/application-java/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/asset-transfer-basic/application-java/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/asset-transfer-basic/application-java/build.gradle b/asset-transfer-basic/application-java/build.gradle new file mode 100644 index 0000000..e2eed62 --- /dev/null +++ b/asset-transfer-basic/application-java/build.gradle @@ -0,0 +1,43 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java project to get you started. + * For more details take a look at the Java Quickstart chapter in the Gradle + * User Manual available at https://docs.gradle.org/6.5/userguide/tutorial_java_projects.html + */ + +plugins { + // Apply the java plugin to add support for Java + id 'java' + + // Apply the application plugin to add support for building a CLI application. + id 'application' +} +ext { + javaMainClass = "application.java.App" +} + +repositories { + // Use jcenter for resolving dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} + +dependencies { + // This dependency is used by the application. + implementation 'com.google.guava:guava:29.0-jre' + implementation 'org.hyperledger.fabric:fabric-gateway-java:2.1.1' +} + +application { + // Define the main class for the application. + mainClassName = 'application.java.App' +} + +// task for running the app after building dependencies +task runApp(type: Exec) { + dependsOn build + group = "Execution" + description = "Run the main class with ExecTask" + commandLine "java", "-classpath", sourceSets.main.runtimeClasspath.getAsPath(), javaMainClass +} \ No newline at end of file diff --git a/asset-transfer-basic/application-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-basic/application-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/asset-transfer-basic/application-java/gradle/wrapper/gradle-wrapper.properties b/asset-transfer-basic/application-java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..622ab64 --- /dev/null +++ b/asset-transfer-basic/application-java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/asset-transfer-basic/application-java/gradlew b/asset-transfer-basic/application-java/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/asset-transfer-basic/application-java/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/asset-transfer-basic/application-java/gradlew.bat b/asset-transfer-basic/application-java/gradlew.bat new file mode 100644 index 0000000..5093609 --- /dev/null +++ b/asset-transfer-basic/application-java/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/asset-transfer-basic/application-java/settings.gradle b/asset-transfer-basic/application-java/settings.gradle new file mode 100644 index 0000000..5423bc7 --- /dev/null +++ b/asset-transfer-basic/application-java/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.5/userguide/multi_project_builds.html + */ + +rootProject.name = 'application-java' diff --git a/asset-transfer-basic/application-java/src/main/java/application/java/App.java b/asset-transfer-basic/application-java/src/main/java/application/java/App.java new file mode 100644 index 0000000..a0d1d5c --- /dev/null +++ b/asset-transfer-basic/application-java/src/main/java/application/java/App.java @@ -0,0 +1,116 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// Running TestApp: +// gradle runApp + +package application.java; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.hyperledger.fabric.gateway.Contract; +import org.hyperledger.fabric.gateway.Gateway; +import org.hyperledger.fabric.gateway.Network; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; + + +public class App { + + static { + System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true"); + } + + // helper function for getting connected to the gateway + public static Gateway connect() throws Exception{ + // Load a file system based wallet for managing identities. + Path walletPath = Paths.get("wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + // load a CCP + Path networkConfigPath = Paths.get("..", "..", "test-network", "organizations", "peerOrganizations", "org1.example.com", "connection-org1.yaml"); + + Gateway.Builder builder = Gateway.createBuilder(); + builder.identity(wallet, "appUser").networkConfig(networkConfigPath).discovery(true); + return builder.connect(); + } + + public static void main(String[] args) throws Exception { + // enrolls the admin and registers the user + try { + EnrollAdmin.main(null); + RegisterUser.main(null); + } catch (Exception e) { + System.err.println(e); + } + + // connect to the network and invoke the smart contract + try (Gateway gateway = connect()) { + + // get the network and contract + Network network = gateway.getNetwork("mychannel"); + Contract contract = network.getContract("basic"); + + byte[] result; + + System.out.println("Submit Transaction: InitLedger creates the initial set of assets on the ledger."); + contract.submitTransaction("InitLedger"); + + System.out.println("\n"); + result = contract.evaluateTransaction("GetAllAssets"); + System.out.println("Evaluate Transaction: GetAllAssets, result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Submit Transaction: CreateAsset asset13"); + //CreateAsset creates an asset with ID asset13, color yellow, owner Tom, size 5 and appraisedValue of 1300 + contract.submitTransaction("CreateAsset", "asset13", "yellow", "5", "Tom", "1300"); + + System.out.println("\n"); + System.out.println("Evaluate Transaction: ReadAsset asset13"); + // ReadAsset returns an asset with given assetID + result = contract.evaluateTransaction("ReadAsset", "asset13"); + System.out.println("result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Evaluate Transaction: AssetExists asset1"); + // AssetExists returns "true" if an asset with given assetID exist + result = contract.evaluateTransaction("AssetExists", "asset1"); + System.out.println("result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Submit Transaction: UpdateAsset asset1, new AppraisedValue : 350"); + // UpdateAsset updates an existing asset with new properties. Same args as CreateAsset + contract.submitTransaction("UpdateAsset", "asset1", "blue", "5", "Tomoko", "350"); + + System.out.println("\n"); + System.out.println("Evaluate Transaction: ReadAsset asset1"); + result = contract.evaluateTransaction("ReadAsset", "asset1"); + System.out.println("result: " + new String(result)); + + try { + System.out.println("\n"); + System.out.println("Submit Transaction: UpdateAsset asset70"); + //Non existing asset asset70 should throw Error + contract.submitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300"); + } catch (Exception e) { + System.err.println("Expected an error on UpdateAsset of non-existing Asset: " + e); + } + + System.out.println("\n"); + System.out.println("Submit Transaction: TransferAsset asset1 from owner Tomoko > owner Tom"); + // TransferAsset transfers an asset with given ID to new owner Tom + contract.submitTransaction("TransferAsset", "asset1", "Tom"); + + System.out.println("\n"); + System.out.println("Evaluate Transaction: ReadAsset asset1"); + result = contract.evaluateTransaction("ReadAsset", "asset1"); + System.out.println("result: " + new String(result)); + } + catch(Exception e){ + System.err.println(e); + } + + } +} diff --git a/asset-transfer-basic/application-java/src/main/java/application/java/EnrollAdmin.java b/asset-transfer-basic/application-java/src/main/java/application/java/EnrollAdmin.java new file mode 100644 index 0000000..563a35f --- /dev/null +++ b/asset-transfer-basic/application-java/src/main/java/application/java/EnrollAdmin.java @@ -0,0 +1,53 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package application.java; + +import java.nio.file.Paths; +import java.util.Properties; + +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Identity; +import org.hyperledger.fabric.sdk.Enrollment; +import org.hyperledger.fabric.sdk.security.CryptoSuite; +import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory; +import org.hyperledger.fabric_ca.sdk.EnrollmentRequest; +import org.hyperledger.fabric_ca.sdk.HFCAClient; + +public class EnrollAdmin { + + public static void main(String[] args) throws Exception { + + // Create a CA client for interacting with the CA. + Properties props = new Properties(); + props.put("pemFile", + "../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem"); + props.put("allowAllHostNames", "true"); + HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props); + CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite(); + caClient.setCryptoSuite(cryptoSuite); + + // Create a wallet for managing identities + Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet")); + + // Check to see if we've already enrolled the admin user. + if (wallet.get("admin") != null) { + System.out.println("An identity for the admin user \"admin\" already exists in the wallet"); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + final EnrollmentRequest enrollmentRequestTLS = new EnrollmentRequest(); + enrollmentRequestTLS.addHost("localhost"); + enrollmentRequestTLS.setProfile("tls"); + Enrollment enrollment = caClient.enroll("admin", "adminpw", enrollmentRequestTLS); + Identity user = Identities.newX509Identity("Org1MSP", enrollment); + wallet.put("admin", user); + System.out.println("Successfully enrolled user \"admin\" and imported it into the wallet"); + } +} diff --git a/asset-transfer-basic/application-java/src/main/java/application/java/RegisterUser.java b/asset-transfer-basic/application-java/src/main/java/application/java/RegisterUser.java new file mode 100644 index 0000000..367b4a3 --- /dev/null +++ b/asset-transfer-basic/application-java/src/main/java/application/java/RegisterUser.java @@ -0,0 +1,107 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package application.java; + +import java.nio.file.Paths; +import java.security.PrivateKey; +import java.util.Properties; +import java.util.Set; + +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Identity; +import org.hyperledger.fabric.gateway.X509Identity; +import org.hyperledger.fabric.sdk.Enrollment; +import org.hyperledger.fabric.sdk.User; +import org.hyperledger.fabric.sdk.security.CryptoSuite; +import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory; +import org.hyperledger.fabric_ca.sdk.HFCAClient; +import org.hyperledger.fabric_ca.sdk.RegistrationRequest; + +public class RegisterUser { + + public static void main(String[] args) throws Exception { + + // Create a CA client for interacting with the CA. + Properties props = new Properties(); + props.put("pemFile", + "../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem"); + props.put("allowAllHostNames", "true"); + HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props); + CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite(); + caClient.setCryptoSuite(cryptoSuite); + + // Create a wallet for managing identities + Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet")); + + // Check to see if we've already enrolled the user. + if (wallet.get("appUser") != null) { + System.out.println("An identity for the user \"appUser\" already exists in the wallet"); + return; + } + + X509Identity adminIdentity = (X509Identity)wallet.get("admin"); + if (adminIdentity == null) { + System.out.println("\"admin\" needs to be enrolled and added to the wallet first"); + return; + } + User admin = new User() { + + @Override + public String getName() { + return "admin"; + } + + @Override + public Set getRoles() { + return null; + } + + @Override + public String getAccount() { + return null; + } + + @Override + public String getAffiliation() { + return "org1.department1"; + } + + @Override + public Enrollment getEnrollment() { + return new Enrollment() { + + @Override + public PrivateKey getKey() { + return adminIdentity.getPrivateKey(); + } + + @Override + public String getCert() { + return Identities.toPemString(adminIdentity.getCertificate()); + } + }; + } + + @Override + public String getMspId() { + return "Org1MSP"; + } + + }; + + // Register the user, enroll the user, and import the new identity into the wallet. + RegistrationRequest registrationRequest = new RegistrationRequest("appUser"); + registrationRequest.setAffiliation("org1.department1"); + registrationRequest.setEnrollmentID("appUser"); + String enrollmentSecret = caClient.register(registrationRequest, admin); + Enrollment enrollment = caClient.enroll("appUser", enrollmentSecret); + Identity user = Identities.newX509Identity("Org1MSP", enrollment); + wallet.put("appUser", user); + System.out.println("Successfully enrolled user \"appUser\" and imported it into the wallet"); + } + +} diff --git a/asset-transfer-basic/application-java/src/main/resources/log4j.properties b/asset-transfer-basic/application-java/src/main/resources/log4j.properties new file mode 100644 index 0000000..f1f841f --- /dev/null +++ b/asset-transfer-basic/application-java/src/main/resources/log4j.properties @@ -0,0 +1,19 @@ +# initialize root logger with level ERROR for stdout and fout +log4j.rootLogger=ERROR,stdout,fout +# set the log level for these components +log4j.logger.com.endeca=INFO +log4j.logger.com.endeca.itl.web.metrics=INFO + +# add a ConsoleAppender to the logger stdout to write to the console +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +# use a simple message format +log4j.appender.stdout.layout.ConversionPattern=%m%n + +# add a FileAppender to the logger fout +log4j.appender.fout=org.apache.log4j.FileAppender +# create a log file +log4j.appender.fout.File=crawl.log +log4j.appender.fout.layout=org.apache.log4j.PatternLayout +# use a more detailed message pattern +log4j.appender.fout.layout.ConversionPattern=%p\t%d{ISO8601}\t%r\t%c\t[%t]\t%m%n diff --git a/asset-transfer-basic/application-javascript/.eslintignore b/asset-transfer-basic/application-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/asset-transfer-basic/application-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-basic/application-javascript/.eslintrc.js b/asset-transfer-basic/application-javascript/.eslintrc.js new file mode 100644 index 0000000..6fa636b --- /dev/null +++ b/asset-transfer-basic/application-javascript/.eslintrc.js @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +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', + 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'] + } +}; diff --git a/asset-transfer-basic/application-javascript/.gitignore b/asset-transfer-basic/application-javascript/.gitignore new file mode 100644 index 0000000..21b287f --- /dev/null +++ b/asset-transfer-basic/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-basic/application-javascript/app.js b/asset-transfer-basic/application-javascript/app.js new file mode 100644 index 0000000..fc8eeca --- /dev/null +++ b/asset-transfer-basic/application-javascript/app.js @@ -0,0 +1,180 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const channelName = 'mychannel'; +const chaincodeName = 'basic'; +const mspOrg1 = 'Org1MSP'; +const walletPath = path.join(__dirname, 'wallet'); +const org1UserId = 'appUser'; + +function prettyJSONString(inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); +} + +// 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 -ccp ../asset-transfer-basic/chaincode-javascript/ -ccl javascript +// - Be sure that node.js is installed +// ===> from directory /fabric-samples/asset-transfer-basic/application-javascript +// node -v +// - npm installed code dependencies +// ===> from directory /fabric-samples/asset-transfer-basic/application-javascript +// npm install +// - to run this test application +// ===> from directory /fabric-samples/asset-transfer-basic/application-javascript +// node app.js + +// 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-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. +// + +/** + * 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(FabricCAServices, ccp, 'ca.org1.example.com'); + + // setup the wallet to hold the credentials of the application user + const wallet = await buildWallet(Wallets, 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(); + + 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, { + wallet, + identity: org1UserId, + discovery: { enabled: true, asLocalhost: true } // using asLocalhost as this gateway is using a fabric network deployed locally + }); + + // 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'); + result = await contract.submitTransaction('CreateAsset', 'asset13', 'yellow', '5', 'Tom', '1300'); + console.log('*** Result: committed'); + if (`${result}` !== '') { + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + } + + 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-javascript/package.json b/asset-transfer-basic/application-javascript/package.json new file mode 100644 index 0000000..bab813d --- /dev/null +++ b/asset-transfer-basic/application-javascript/package.json @@ -0,0 +1,16 @@ +{ + "name": "asset-transfer-basic", + "version": "1.0.0", + "description": "Asset-transfer-basic application implemented in JavaScript", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4" + } +} diff --git a/asset-transfer-basic/application-typescript/.gitignore b/asset-transfer-basic/application-typescript/.gitignore new file mode 100644 index 0000000..48285d1 --- /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.json b/asset-transfer-basic/application-typescript/package.json new file mode 100644 index 0000000..91a9c6b --- /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.4", + "fabric-network": "^2.2.4" + }, + "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 0000000..075e0ed --- /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 -ccp ../asset-transfer-basic/chaincode-typescript/ -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 0000000..f284e30 --- /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 0000000..665dd0e --- /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 0000000..8d35748 --- /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 0000000..a52c3ee --- /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/asset-transfer-basic/chaincode-external/.dockerignore b/asset-transfer-basic/chaincode-external/.dockerignore new file mode 100644 index 0000000..61260e5 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/.dockerignore @@ -0,0 +1,5 @@ +chaincode.env* +*.json +*.md +*.tar.gz +*.tgz diff --git a/asset-transfer-basic/chaincode-external/.gitignore b/asset-transfer-basic/chaincode-external/.gitignore new file mode 100644 index 0000000..9db4ee6 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/.gitignore @@ -0,0 +1,2 @@ +*.tar.gz +*.tgz diff --git a/asset-transfer-basic/chaincode-external/Dockerfile b/asset-transfer-basic/chaincode-external/Dockerfile new file mode 100644 index 0000000..833b14e --- /dev/null +++ b/asset-transfer-basic/chaincode-external/Dockerfile @@ -0,0 +1,17 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +ARG GO_VER=1.14.4 +ARG ALPINE_VER=3.12 + +FROM golang:${GO_VER}-alpine${ALPINE_VER} + +WORKDIR /go/src/github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-external +COPY . . + +RUN go get -d -v ./... +RUN go install -v ./... + +EXPOSE 9999 +CMD ["chaincode-external"] diff --git a/asset-transfer-basic/chaincode-external/README.md b/asset-transfer-basic/chaincode-external/README.md new file mode 100755 index 0000000..1b4a294 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/README.md @@ -0,0 +1,161 @@ +# Asset-Transfer-Basic 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. + +## Setting up the external builder and launcher + +External Builders and Launchers is an advanced feature that typically requires custom packaging of the peer image so that it contains all the tools your builder and launcher require. For this sample we use very simple (and crude) shell scripts that can be run directly within the default Fabric peer images. + +Open the `config/core.yaml` file at the top of the `fabric-samples` hierarchy. Note that this file comes along with the Fabric binaries, so if you don't have it, follow the [Install the Samples, Binaries and Docker Images](https://hyperledger-fabric.readthedocs.io/en/latest/install.html) instructions in the Hyperledger Fabric documentation to download the binaries and config files. + +Modify the field `externalBuilders` as the following: +``` +externalBuilders: + - path: /opt/gopath/src/github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-external/sampleBuilder + name: external-sample-builder +``` +This configuration sets the name of the external builder as `external-sample-builder`, and the path of the builder to the scripts provided in this sample. Note that this is the path within the peer container, not your local machine. + +To set the path within the peer container, you will need to modify the container compose file to mount a couple of additional volumes. +Open the file `test-network/docker/docker-compose-test-net.yaml`, and add to the `volumes` section of both `peer0.org1.example.com` and `peer0.org2.example.com` the following two lines: + +``` + - ../..:/opt/gopath/src/github.com/hyperledger/fabric-samples + - ../../config/core.yaml:/etc/hyperledger/fabric/core.yaml +``` + +This will mount the fabric-sample builder into the peer container so that it can be found at the location specified in the config file, +and override the peer's core.yaml config file within the fabric-peer image so that the config file modified above is used. + +## Packaging and installing Chaincode + +The Asset-Transfer-Basic external chaincode requires two environment variables to run, `CHAINCODE_SERVER_ADDRESS` and `CHAINCODE_ID`, which are described and set in the `chaincode.env` file. + +The peer needs a corresponding `connection.json` configuration file so that it can connect to the external Asset-Transfer-Basic service. + +The address specified in the `connection.json` must correspond to the `CHAINCODE_SERVER_ADDRESS` value in `chaincode.env`, which is `asset-transfer-basic.org1.example.com:9999` in our example. + +Because we will run our chaincode as an external service, the chaincode itself does not need to be included in the chaincode +package that gets installed to each peer. Only the configuration and metadata information needs to be included +in the package. Since the packaging is trivial, we can manually create the chaincode package. + +First, create a `code.tar.gz` archive containing the `connection.json` file: + +``` +tar cfz code.tar.gz connection.json +``` + +Then, create the chaincode package, including the `code.tar.gz` file and the supplied `metadata.json` file: + +``` +tar cfz asset-transfer-basic-external.tgz metadata.json code.tar.gz +``` + +You are now ready to use the external chaincode. We will use the `test-network` sample to get a network setup and make use of it. + +## Starting the test network + +In a different terminal, from the `test-network` sample directory starts the network using the following command: + +``` +./network.sh up createChannel -c mychannel -ca +``` + +This starts the test network and creates the channel. We will now proceed to installing our external chaincode package. + +## Installing the external chaincode + +We can't use the `test-network/network.sh` script to install our external chaincode so we will have to do a bit more work by hand but we can still leverage part of the test-network scripts to make this easier. + +First, get the functions to setup your environment as needed by running the following command (this assumes you are still in the `test-network` directory): + +``` +. ./scripts/envVar.sh +``` + +Install the `asset-transfer-basic-external.tar.gz` chaincode on org1: + +``` +setGlobals 1 +../bin/peer lifecycle chaincode install ../asset-transfer-basic/chaincode-external/asset-transfer-basic-external.tgz +``` + +setGlobals simply defines a bunch of environment variables suitable to act as one organization or another, org1 or org2. + +Install it on org2: + +``` +setGlobals 2 +../bin/peer lifecycle chaincode install ../asset-transfer-basic/chaincode-external/asset-transfer-basic-external.tgz +``` + +This will output the chaincode pakage identifier such as `basic_1.0:0262396ccaffaa2174bc09f750f742319c4f14d60b16334d2c8921b6842c090c` that you will need to use in the following commands. + +For convenience it is best to store your package id value in an environment variable so that it can be referenced in later commands: + +``` +export PKGID=basic_1.0:0262396ccaffaa2174bc09f750f742319c4f14d60b16334d2c8921b6842c090 +``` + +If needed, you can query the installed chaincode to get its package-id: + +``` +setGlobals 1 +../bin/peer lifecycle chaincode queryinstalled --peerAddresses localhost:7051 --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +``` + +Edit the `chaincode.env` file in the `fabric-samples/asset-transfer-basic/chaincode-external` directory as necessary to set the `CHAINCODE_ID` variable to the chaincode package-id obtained above. + + +## Running the Asset-Transfer-Basic external service + +To run the service in a container, from a different terminal, build an Asset-Transfer-Basic docker image, using the supplied `Dockerfile`, using the following command in the `fabric-samples/asset-transfer-basic/chaincode-external` directory: + +``` +docker build -t hyperledger/asset-transfer-basic . +``` + +Then, start the Asset-Transfer-Basic service: + +``` +docker run -it --rm --name asset-transfer-basic.org1.example.com --hostname asset-transfer-basic.org1.example.com --env-file chaincode.env --network=net_test hyperledger/asset-transfer-basic +``` + +This will start the container and start the external chaincode service within it. + +## Finish deploying the Asset-Transfer-Basic external chaincode + +Finishing the deployment of the chaincode on the test network can be done from the terminal you started the network from with the following commands (make sure the package-id is set to the value you received above): + +``` +setGlobals 2 +../bin/peer lifecycle chaincode approveformyorg -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 --channelID mychannel --name basic --version 1.0 --package-id $PKGID --sequence 1 + +setGlobals 1 +../bin/peer lifecycle chaincode approveformyorg -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 --channelID mychannel --name basic --version 1.0 --package-id $PKGID --sequence 1 + +../bin/peer lifecycle chaincode commit -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 --channelID mychannel --name basic --peerAddresses localhost:7051 --tlsRootCertFiles $PWD/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt --version 1.0 --sequence 1 +``` + +This approves the chaincode definition for both orgs and commits it using org1. This should result in an output similar to: + +``` +2020-08-05 15:41:44.982 PDT [chaincodeCmd] ClientWait -> INFO 001 txid [6bdbe040b99a45cc90a23ec21f02ea5da7be8b70590eb04ff3323ef77fdedfc7] committed with status (VALID) at localhost:7051 +2020-08-05 15:41:44.983 PDT [chaincodeCmd] ClientWait -> INFO 002 txid [6bdbe040b99a45cc90a23ec21f02ea5da7be8b70590eb04ff3323ef77fdedfc7] committed with status (VALID) at localhost:9051 +``` + +Now that the chaincode is deployed to the channel, and started as an external service, it can be used as normal. + +## Using the Asset-Transfer-Basic external chaincode + +From yet another terminal, go to `fabric-samples/asset-transfer-basic/application-javascript` and use the node application to test the chaincode you just installed with the following commands: + +``` +rm -rf wallet # in case you ran this before +npm install +node app.js +``` + +If all goes well, it should run exactly the same as described in the "Writing Your First Application" tutorial. diff --git a/asset-transfer-basic/chaincode-external/assetTransfer.go b/asset-transfer-basic/chaincode-external/assetTransfer.go new file mode 100644 index 0000000..d66e073 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/assetTransfer.go @@ -0,0 +1,235 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +type serverConfig struct { + CCID string + Address string +} + +// SmartContract provides functions for managing an asset +type SmartContract struct { + contractapi.Contract +} + +// Asset describes basic details of what makes up a simple asset +type Asset struct { + ID string `json:"ID"` + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` + AppraisedValue int `json:"appraisedValue"` +} + +// QueryResult structure used for handling result of query +type QueryResult struct { + Key string `json:"Key"` + Record *Asset +} + +// InitLedger adds a base set of cars to the ledger +func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { + assets := []Asset{ + {ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300}, + {ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400}, + {ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500}, + {ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600}, + {ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700}, + {ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800}, + } + + for _, asset := range assets { + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(asset.ID, assetJSON) + if err != nil { + return fmt.Errorf("failed to put to world state: %v", err) + } + } + + return nil +} + +// CreateAsset issues a new asset to the world state with given details. +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id, color string, size int, owner string, appraisedValue int) error { + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if exists { + return fmt.Errorf("the asset %s already exists", id) + } + asset := Asset{ + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + } + + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// ReadAsset returns the asset stored in the world state with given id. +func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) { + assetJSON, err := ctx.GetStub().GetState(id) + if err != nil { + return nil, fmt.Errorf("failed to read from world state. %s", err.Error()) + } + if assetJSON == nil { + return nil, fmt.Errorf("the asset %s does not exist", id) + } + + var asset Asset + err = json.Unmarshal(assetJSON, &asset) + if err != nil { + return nil, err + } + + return &asset, nil +} + +// UpdateAsset updates an existing asset in the world state with provided parameters. +func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id, color string, size int, owner string, appraisedValue int) error { + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("the asset %s does not exist", id) + } + + // overwritting original asset with new asset + asset := Asset{ + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + } + + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// DeleteAsset deletes an given asset from the world state. +func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error { + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("the asset %s does not exist", id) + } + + return ctx.GetStub().DelState(id) +} + +// AssetExists returns true when asset with given ID exists in world state +func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) { + assetJSON, err := ctx.GetStub().GetState(id) + if err != nil { + return false, fmt.Errorf("failed to read from world state. %s", err.Error()) + } + + return assetJSON != nil, nil +} + +// TransferAsset updates the owner field of asset with given id in world state. +func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error { + asset, err := s.ReadAsset(ctx, id) + if err != nil { + return err + } + + asset.Owner = newOwner + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// GetAllAssets returns all assets found in world state +func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) { + // range query with empty string for startKey and endKey does an open-ended query of all assets in the chaincode namespace. + resultsIterator, err := ctx.GetStub().GetStateByRange("", "") + + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + var results []QueryResult + + for resultsIterator.HasNext() { + queryResponse, err := resultsIterator.Next() + + if err != nil { + return nil, err + } + + var asset Asset + err = json.Unmarshal(queryResponse.Value, &asset) + if err != nil { + return nil, err + } + + queryResult := QueryResult{Key: queryResponse.Key, Record: &asset} + results = append(results, queryResult) + } + + return results, nil +} + +func main() { + // See chaincode.env.example + config := serverConfig{ + CCID: os.Getenv("CHAINCODE_ID"), + Address: os.Getenv("CHAINCODE_SERVER_ADDRESS"), + } + + chaincode, err := contractapi.NewChaincode(&SmartContract{}) + + if err != nil { + log.Panicf("error create asset-transfer-basic chaincode: %s", err) + } + + server := &shim.ChaincodeServer{ + CCID: config.CCID, + Address: config.Address, + CC: chaincode, + TLSProps: shim.TLSProperties{ + Disabled: true, + }, + } + + if err := server.Start(); err != nil { + log.Panicf("error starting asset-transfer-basic chaincode: %s", err) + } +} diff --git a/asset-transfer-basic/chaincode-external/chaincode.env b/asset-transfer-basic/chaincode-external/chaincode.env new file mode 100644 index 0000000..d029f58 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/chaincode.env @@ -0,0 +1,8 @@ +# CHAINCODE_SERVER_ADDRESS must be set to the host and port where the peer can +# connect to the chaincode server +CHAINCODE_SERVER_ADDRESS=asset-transfer-basic.org1.example.com:9999 + +# CHAINCODE_ID must be set to the Package ID that is assigned to the chaincode +# on install. The `peer lifecycle chaincode queryinstalled` command can be +# used to get the ID after install if required +CHAINCODE_ID=basic_1.0:0262396ccaffaa2174bc09f750f742319c4f14d60b16334d2c8921b6842c090c diff --git a/asset-transfer-basic/chaincode-external/connection.json b/asset-transfer-basic/chaincode-external/connection.json new file mode 100644 index 0000000..d97f8f1 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/connection.json @@ -0,0 +1,5 @@ +{ + "address": "asset-transfer-basic.org1.example.com:9999", + "dial_timeout": "10s", + "tls_required": false +} diff --git a/asset-transfer-basic/chaincode-external/go.mod b/asset-transfer-basic/chaincode-external/go.mod new file mode 100644 index 0000000..d8e3eec --- /dev/null +++ b/asset-transfer-basic/chaincode-external/go.mod @@ -0,0 +1,8 @@ +module github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-external + +go 1.14 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212 + github.com/hyperledger/fabric-contract-api-go v1.1.0 +) diff --git a/asset-transfer-basic/chaincode-external/go.sum b/asset-transfer-basic/chaincode-external/go.sum new file mode 100644 index 0000000..a159a45 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/go.sum @@ -0,0 +1,146 @@ +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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/asset-transfer-basic/chaincode-external/metadata.json b/asset-transfer-basic/chaincode-external/metadata.json new file mode 100644 index 0000000..ae23860 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/metadata.json @@ -0,0 +1,4 @@ +{ + "type": "external", + "label": "basic_1.0" +} diff --git a/asset-transfer-basic/chaincode-external/sampleBuilder/bin/detect b/asset-transfer-basic/chaincode-external/sampleBuilder/bin/detect new file mode 100755 index 0000000..d4b7007 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/sampleBuilder/bin/detect @@ -0,0 +1,14 @@ +#!/bin/sh + +set -euo pipefail + +METADIR=$2 +# check if the "type" field is set to "external" +# crude way without jq which is not in the default fabric peer image +TYPE=$(tr -d '\n' < "$METADIR/metadata.json" | awk -F':' '{ for (i = 1; i < NF; i++){ if ($i~/type/) { print $(i+1); break }}}'| cut -d\" -f2) + +if [ "$TYPE" = "external" ]; then + exit 0 +fi + +exit 1 diff --git a/asset-transfer-basic/chaincode-external/sampleBuilder/bin/release b/asset-transfer-basic/chaincode-external/sampleBuilder/bin/release new file mode 100755 index 0000000..7f03a05 --- /dev/null +++ b/asset-transfer-basic/chaincode-external/sampleBuilder/bin/release @@ -0,0 +1,22 @@ +#!/bin/sh + +set -euo pipefail + +BLD="$1" +RELEASE="$2" + +if [ -d "$BLD/metadata" ]; then + cp -a "$BLD/metadata/"* "$RELEASE/" +fi + +#external chaincodes expect artifacts to be placed under "$RELEASE"/chaincode/server +if [ -f $BLD/connection.json ]; then + mkdir -p "$RELEASE"/chaincode/server + cp $BLD/connection.json "$RELEASE"/chaincode/server + + #if tls_required is true, copy TLS files (using above example, the fully qualified path for these fils would be "$RELEASE"/chaincode/server/tls) + + exit 0 +fi + +exit 1 diff --git a/asset-transfer-basic/chaincode-go/assetTransfer.go b/asset-transfer-basic/chaincode-go/assetTransfer.go new file mode 100644 index 0000000..9c619d5 --- /dev/null +++ b/asset-transfer-basic/chaincode-go/assetTransfer.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/asset-transfer-basic/chaincode-go/chaincode" +) + +func main() { + assetChaincode, err := contractapi.NewChaincode(&chaincode.SmartContract{}) + if err != nil { + log.Panicf("Error creating asset-transfer-basic chaincode: %v", err) + } + + if err := assetChaincode.Start(); err != nil { + log.Panicf("Error starting asset-transfer-basic chaincode: %v", err) + } +} diff --git a/asset-transfer-basic/chaincode-go/chaincode/mocks/chaincodestub.go b/asset-transfer-basic/chaincode-go/chaincode/mocks/chaincodestub.go new file mode 100644 index 0000000..91348fe --- /dev/null +++ b/asset-transfer-basic/chaincode-go/chaincode/mocks/chaincodestub.go @@ -0,0 +1,2878 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-protos-go/peer" +) + +type ChaincodeStub struct { + CreateCompositeKeyStub func(string, []string) (string, error) + createCompositeKeyMutex sync.RWMutex + createCompositeKeyArgsForCall []struct { + arg1 string + arg2 []string + } + createCompositeKeyReturns struct { + result1 string + result2 error + } + createCompositeKeyReturnsOnCall map[int]struct { + result1 string + result2 error + } + DelPrivateDataStub func(string, string) error + delPrivateDataMutex sync.RWMutex + delPrivateDataArgsForCall []struct { + arg1 string + arg2 string + } + delPrivateDataReturns struct { + result1 error + } + delPrivateDataReturnsOnCall map[int]struct { + result1 error + } + DelStateStub func(string) error + delStateMutex sync.RWMutex + delStateArgsForCall []struct { + arg1 string + } + delStateReturns struct { + result1 error + } + delStateReturnsOnCall map[int]struct { + result1 error + } + GetArgsStub func() [][]byte + getArgsMutex sync.RWMutex + getArgsArgsForCall []struct { + } + getArgsReturns struct { + result1 [][]byte + } + getArgsReturnsOnCall map[int]struct { + result1 [][]byte + } + GetArgsSliceStub func() ([]byte, error) + getArgsSliceMutex sync.RWMutex + getArgsSliceArgsForCall []struct { + } + getArgsSliceReturns struct { + result1 []byte + result2 error + } + getArgsSliceReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetBindingStub func() ([]byte, error) + getBindingMutex sync.RWMutex + getBindingArgsForCall []struct { + } + getBindingReturns struct { + result1 []byte + result2 error + } + getBindingReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetChannelIDStub func() string + getChannelIDMutex sync.RWMutex + getChannelIDArgsForCall []struct { + } + getChannelIDReturns struct { + result1 string + } + getChannelIDReturnsOnCall map[int]struct { + result1 string + } + GetCreatorStub func() ([]byte, error) + getCreatorMutex sync.RWMutex + getCreatorArgsForCall []struct { + } + getCreatorReturns struct { + result1 []byte + result2 error + } + getCreatorReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetDecorationsStub func() map[string][]byte + getDecorationsMutex sync.RWMutex + getDecorationsArgsForCall []struct { + } + getDecorationsReturns struct { + result1 map[string][]byte + } + getDecorationsReturnsOnCall map[int]struct { + result1 map[string][]byte + } + GetFunctionAndParametersStub func() (string, []string) + getFunctionAndParametersMutex sync.RWMutex + getFunctionAndParametersArgsForCall []struct { + } + getFunctionAndParametersReturns struct { + result1 string + result2 []string + } + getFunctionAndParametersReturnsOnCall map[int]struct { + result1 string + result2 []string + } + GetHistoryForKeyStub func(string) (shim.HistoryQueryIteratorInterface, error) + getHistoryForKeyMutex sync.RWMutex + getHistoryForKeyArgsForCall []struct { + arg1 string + } + getHistoryForKeyReturns struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + } + getHistoryForKeyReturnsOnCall map[int]struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + } + GetPrivateDataStub func(string, string) ([]byte, error) + getPrivateDataMutex sync.RWMutex + getPrivateDataArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataReturns struct { + result1 []byte + result2 error + } + getPrivateDataReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetPrivateDataByPartialCompositeKeyStub func(string, string, []string) (shim.StateQueryIteratorInterface, error) + getPrivateDataByPartialCompositeKeyMutex sync.RWMutex + getPrivateDataByPartialCompositeKeyArgsForCall []struct { + arg1 string + arg2 string + arg3 []string + } + getPrivateDataByPartialCompositeKeyReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getPrivateDataByPartialCompositeKeyReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetPrivateDataByRangeStub func(string, string, string) (shim.StateQueryIteratorInterface, error) + getPrivateDataByRangeMutex sync.RWMutex + getPrivateDataByRangeArgsForCall []struct { + arg1 string + arg2 string + arg3 string + } + getPrivateDataByRangeReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getPrivateDataByRangeReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetPrivateDataHashStub func(string, string) ([]byte, error) + getPrivateDataHashMutex sync.RWMutex + getPrivateDataHashArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataHashReturns struct { + result1 []byte + result2 error + } + getPrivateDataHashReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetPrivateDataQueryResultStub func(string, string) (shim.StateQueryIteratorInterface, error) + getPrivateDataQueryResultMutex sync.RWMutex + getPrivateDataQueryResultArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataQueryResultReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getPrivateDataQueryResultReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetPrivateDataValidationParameterStub func(string, string) ([]byte, error) + getPrivateDataValidationParameterMutex sync.RWMutex + getPrivateDataValidationParameterArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataValidationParameterReturns struct { + result1 []byte + result2 error + } + getPrivateDataValidationParameterReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetQueryResultStub func(string) (shim.StateQueryIteratorInterface, error) + getQueryResultMutex sync.RWMutex + getQueryResultArgsForCall []struct { + arg1 string + } + getQueryResultReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getQueryResultReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetQueryResultWithPaginationStub func(string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) + getQueryResultWithPaginationMutex sync.RWMutex + getQueryResultWithPaginationArgsForCall []struct { + arg1 string + arg2 int32 + arg3 string + } + getQueryResultWithPaginationReturns struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + getQueryResultWithPaginationReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + GetSignedProposalStub func() (*peer.SignedProposal, error) + getSignedProposalMutex sync.RWMutex + getSignedProposalArgsForCall []struct { + } + getSignedProposalReturns struct { + result1 *peer.SignedProposal + result2 error + } + getSignedProposalReturnsOnCall map[int]struct { + result1 *peer.SignedProposal + result2 error + } + GetStateStub func(string) ([]byte, error) + getStateMutex sync.RWMutex + getStateArgsForCall []struct { + arg1 string + } + getStateReturns struct { + result1 []byte + result2 error + } + getStateReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetStateByPartialCompositeKeyStub func(string, []string) (shim.StateQueryIteratorInterface, error) + getStateByPartialCompositeKeyMutex sync.RWMutex + getStateByPartialCompositeKeyArgsForCall []struct { + arg1 string + arg2 []string + } + getStateByPartialCompositeKeyReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getStateByPartialCompositeKeyReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetStateByPartialCompositeKeyWithPaginationStub func(string, []string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) + getStateByPartialCompositeKeyWithPaginationMutex sync.RWMutex + getStateByPartialCompositeKeyWithPaginationArgsForCall []struct { + arg1 string + arg2 []string + arg3 int32 + arg4 string + } + getStateByPartialCompositeKeyWithPaginationReturns struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + getStateByPartialCompositeKeyWithPaginationReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + GetStateByRangeStub func(string, string) (shim.StateQueryIteratorInterface, error) + getStateByRangeMutex sync.RWMutex + getStateByRangeArgsForCall []struct { + arg1 string + arg2 string + } + getStateByRangeReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getStateByRangeReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetStateByRangeWithPaginationStub func(string, string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) + getStateByRangeWithPaginationMutex sync.RWMutex + getStateByRangeWithPaginationArgsForCall []struct { + arg1 string + arg2 string + arg3 int32 + arg4 string + } + getStateByRangeWithPaginationReturns struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + getStateByRangeWithPaginationReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + GetStateValidationParameterStub func(string) ([]byte, error) + getStateValidationParameterMutex sync.RWMutex + getStateValidationParameterArgsForCall []struct { + arg1 string + } + getStateValidationParameterReturns struct { + result1 []byte + result2 error + } + getStateValidationParameterReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetStringArgsStub func() []string + getStringArgsMutex sync.RWMutex + getStringArgsArgsForCall []struct { + } + getStringArgsReturns struct { + result1 []string + } + getStringArgsReturnsOnCall map[int]struct { + result1 []string + } + GetTransientStub func() (map[string][]byte, error) + getTransientMutex sync.RWMutex + getTransientArgsForCall []struct { + } + getTransientReturns struct { + result1 map[string][]byte + result2 error + } + getTransientReturnsOnCall map[int]struct { + result1 map[string][]byte + result2 error + } + GetTxIDStub func() string + getTxIDMutex sync.RWMutex + getTxIDArgsForCall []struct { + } + getTxIDReturns struct { + result1 string + } + getTxIDReturnsOnCall map[int]struct { + result1 string + } + GetTxTimestampStub func() (*timestamp.Timestamp, error) + getTxTimestampMutex sync.RWMutex + getTxTimestampArgsForCall []struct { + } + getTxTimestampReturns struct { + result1 *timestamp.Timestamp + result2 error + } + getTxTimestampReturnsOnCall map[int]struct { + result1 *timestamp.Timestamp + result2 error + } + InvokeChaincodeStub func(string, [][]byte, string) peer.Response + invokeChaincodeMutex sync.RWMutex + invokeChaincodeArgsForCall []struct { + arg1 string + arg2 [][]byte + arg3 string + } + invokeChaincodeReturns struct { + result1 peer.Response + } + invokeChaincodeReturnsOnCall map[int]struct { + result1 peer.Response + } + PutPrivateDataStub func(string, string, []byte) error + putPrivateDataMutex sync.RWMutex + putPrivateDataArgsForCall []struct { + arg1 string + arg2 string + arg3 []byte + } + putPrivateDataReturns struct { + result1 error + } + putPrivateDataReturnsOnCall map[int]struct { + result1 error + } + PutStateStub func(string, []byte) error + putStateMutex sync.RWMutex + putStateArgsForCall []struct { + arg1 string + arg2 []byte + } + putStateReturns struct { + result1 error + } + putStateReturnsOnCall map[int]struct { + result1 error + } + SetEventStub func(string, []byte) error + setEventMutex sync.RWMutex + setEventArgsForCall []struct { + arg1 string + arg2 []byte + } + setEventReturns struct { + result1 error + } + setEventReturnsOnCall map[int]struct { + result1 error + } + SetPrivateDataValidationParameterStub func(string, string, []byte) error + setPrivateDataValidationParameterMutex sync.RWMutex + setPrivateDataValidationParameterArgsForCall []struct { + arg1 string + arg2 string + arg3 []byte + } + setPrivateDataValidationParameterReturns struct { + result1 error + } + setPrivateDataValidationParameterReturnsOnCall map[int]struct { + result1 error + } + SetStateValidationParameterStub func(string, []byte) error + setStateValidationParameterMutex sync.RWMutex + setStateValidationParameterArgsForCall []struct { + arg1 string + arg2 []byte + } + setStateValidationParameterReturns struct { + result1 error + } + setStateValidationParameterReturnsOnCall map[int]struct { + result1 error + } + SplitCompositeKeyStub func(string) (string, []string, error) + splitCompositeKeyMutex sync.RWMutex + splitCompositeKeyArgsForCall []struct { + arg1 string + } + splitCompositeKeyReturns struct { + result1 string + result2 []string + result3 error + } + splitCompositeKeyReturnsOnCall map[int]struct { + result1 string + result2 []string + result3 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *ChaincodeStub) CreateCompositeKey(arg1 string, arg2 []string) (string, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + fake.createCompositeKeyMutex.Lock() + ret, specificReturn := fake.createCompositeKeyReturnsOnCall[len(fake.createCompositeKeyArgsForCall)] + fake.createCompositeKeyArgsForCall = append(fake.createCompositeKeyArgsForCall, struct { + arg1 string + arg2 []string + }{arg1, arg2Copy}) + fake.recordInvocation("CreateCompositeKey", []interface{}{arg1, arg2Copy}) + fake.createCompositeKeyMutex.Unlock() + if fake.CreateCompositeKeyStub != nil { + return fake.CreateCompositeKeyStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.createCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) CreateCompositeKeyCallCount() int { + fake.createCompositeKeyMutex.RLock() + defer fake.createCompositeKeyMutex.RUnlock() + return len(fake.createCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) CreateCompositeKeyCalls(stub func(string, []string) (string, error)) { + fake.createCompositeKeyMutex.Lock() + defer fake.createCompositeKeyMutex.Unlock() + fake.CreateCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) CreateCompositeKeyArgsForCall(i int) (string, []string) { + fake.createCompositeKeyMutex.RLock() + defer fake.createCompositeKeyMutex.RUnlock() + argsForCall := fake.createCompositeKeyArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) CreateCompositeKeyReturns(result1 string, result2 error) { + fake.createCompositeKeyMutex.Lock() + defer fake.createCompositeKeyMutex.Unlock() + fake.CreateCompositeKeyStub = nil + fake.createCompositeKeyReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) CreateCompositeKeyReturnsOnCall(i int, result1 string, result2 error) { + fake.createCompositeKeyMutex.Lock() + defer fake.createCompositeKeyMutex.Unlock() + fake.CreateCompositeKeyStub = nil + if fake.createCompositeKeyReturnsOnCall == nil { + fake.createCompositeKeyReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.createCompositeKeyReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) DelPrivateData(arg1 string, arg2 string) error { + fake.delPrivateDataMutex.Lock() + ret, specificReturn := fake.delPrivateDataReturnsOnCall[len(fake.delPrivateDataArgsForCall)] + fake.delPrivateDataArgsForCall = append(fake.delPrivateDataArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("DelPrivateData", []interface{}{arg1, arg2}) + fake.delPrivateDataMutex.Unlock() + if fake.DelPrivateDataStub != nil { + return fake.DelPrivateDataStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.delPrivateDataReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) DelPrivateDataCallCount() int { + fake.delPrivateDataMutex.RLock() + defer fake.delPrivateDataMutex.RUnlock() + return len(fake.delPrivateDataArgsForCall) +} + +func (fake *ChaincodeStub) DelPrivateDataCalls(stub func(string, string) error) { + fake.delPrivateDataMutex.Lock() + defer fake.delPrivateDataMutex.Unlock() + fake.DelPrivateDataStub = stub +} + +func (fake *ChaincodeStub) DelPrivateDataArgsForCall(i int) (string, string) { + fake.delPrivateDataMutex.RLock() + defer fake.delPrivateDataMutex.RUnlock() + argsForCall := fake.delPrivateDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) DelPrivateDataReturns(result1 error) { + fake.delPrivateDataMutex.Lock() + defer fake.delPrivateDataMutex.Unlock() + fake.DelPrivateDataStub = nil + fake.delPrivateDataReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) DelPrivateDataReturnsOnCall(i int, result1 error) { + fake.delPrivateDataMutex.Lock() + defer fake.delPrivateDataMutex.Unlock() + fake.DelPrivateDataStub = nil + if fake.delPrivateDataReturnsOnCall == nil { + fake.delPrivateDataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.delPrivateDataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) DelState(arg1 string) error { + fake.delStateMutex.Lock() + ret, specificReturn := fake.delStateReturnsOnCall[len(fake.delStateArgsForCall)] + fake.delStateArgsForCall = append(fake.delStateArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("DelState", []interface{}{arg1}) + fake.delStateMutex.Unlock() + if fake.DelStateStub != nil { + return fake.DelStateStub(arg1) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.delStateReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) DelStateCallCount() int { + fake.delStateMutex.RLock() + defer fake.delStateMutex.RUnlock() + return len(fake.delStateArgsForCall) +} + +func (fake *ChaincodeStub) DelStateCalls(stub func(string) error) { + fake.delStateMutex.Lock() + defer fake.delStateMutex.Unlock() + fake.DelStateStub = stub +} + +func (fake *ChaincodeStub) DelStateArgsForCall(i int) string { + fake.delStateMutex.RLock() + defer fake.delStateMutex.RUnlock() + argsForCall := fake.delStateArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) DelStateReturns(result1 error) { + fake.delStateMutex.Lock() + defer fake.delStateMutex.Unlock() + fake.DelStateStub = nil + fake.delStateReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) DelStateReturnsOnCall(i int, result1 error) { + fake.delStateMutex.Lock() + defer fake.delStateMutex.Unlock() + fake.DelStateStub = nil + if fake.delStateReturnsOnCall == nil { + fake.delStateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.delStateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) GetArgs() [][]byte { + fake.getArgsMutex.Lock() + ret, specificReturn := fake.getArgsReturnsOnCall[len(fake.getArgsArgsForCall)] + fake.getArgsArgsForCall = append(fake.getArgsArgsForCall, struct { + }{}) + fake.recordInvocation("GetArgs", []interface{}{}) + fake.getArgsMutex.Unlock() + if fake.GetArgsStub != nil { + return fake.GetArgsStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getArgsReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetArgsCallCount() int { + fake.getArgsMutex.RLock() + defer fake.getArgsMutex.RUnlock() + return len(fake.getArgsArgsForCall) +} + +func (fake *ChaincodeStub) GetArgsCalls(stub func() [][]byte) { + fake.getArgsMutex.Lock() + defer fake.getArgsMutex.Unlock() + fake.GetArgsStub = stub +} + +func (fake *ChaincodeStub) GetArgsReturns(result1 [][]byte) { + fake.getArgsMutex.Lock() + defer fake.getArgsMutex.Unlock() + fake.GetArgsStub = nil + fake.getArgsReturns = struct { + result1 [][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetArgsReturnsOnCall(i int, result1 [][]byte) { + fake.getArgsMutex.Lock() + defer fake.getArgsMutex.Unlock() + fake.GetArgsStub = nil + if fake.getArgsReturnsOnCall == nil { + fake.getArgsReturnsOnCall = make(map[int]struct { + result1 [][]byte + }) + } + fake.getArgsReturnsOnCall[i] = struct { + result1 [][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetArgsSlice() ([]byte, error) { + fake.getArgsSliceMutex.Lock() + ret, specificReturn := fake.getArgsSliceReturnsOnCall[len(fake.getArgsSliceArgsForCall)] + fake.getArgsSliceArgsForCall = append(fake.getArgsSliceArgsForCall, struct { + }{}) + fake.recordInvocation("GetArgsSlice", []interface{}{}) + fake.getArgsSliceMutex.Unlock() + if fake.GetArgsSliceStub != nil { + return fake.GetArgsSliceStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getArgsSliceReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetArgsSliceCallCount() int { + fake.getArgsSliceMutex.RLock() + defer fake.getArgsSliceMutex.RUnlock() + return len(fake.getArgsSliceArgsForCall) +} + +func (fake *ChaincodeStub) GetArgsSliceCalls(stub func() ([]byte, error)) { + fake.getArgsSliceMutex.Lock() + defer fake.getArgsSliceMutex.Unlock() + fake.GetArgsSliceStub = stub +} + +func (fake *ChaincodeStub) GetArgsSliceReturns(result1 []byte, result2 error) { + fake.getArgsSliceMutex.Lock() + defer fake.getArgsSliceMutex.Unlock() + fake.GetArgsSliceStub = nil + fake.getArgsSliceReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetArgsSliceReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getArgsSliceMutex.Lock() + defer fake.getArgsSliceMutex.Unlock() + fake.GetArgsSliceStub = nil + if fake.getArgsSliceReturnsOnCall == nil { + fake.getArgsSliceReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getArgsSliceReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetBinding() ([]byte, error) { + fake.getBindingMutex.Lock() + ret, specificReturn := fake.getBindingReturnsOnCall[len(fake.getBindingArgsForCall)] + fake.getBindingArgsForCall = append(fake.getBindingArgsForCall, struct { + }{}) + fake.recordInvocation("GetBinding", []interface{}{}) + fake.getBindingMutex.Unlock() + if fake.GetBindingStub != nil { + return fake.GetBindingStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getBindingReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetBindingCallCount() int { + fake.getBindingMutex.RLock() + defer fake.getBindingMutex.RUnlock() + return len(fake.getBindingArgsForCall) +} + +func (fake *ChaincodeStub) GetBindingCalls(stub func() ([]byte, error)) { + fake.getBindingMutex.Lock() + defer fake.getBindingMutex.Unlock() + fake.GetBindingStub = stub +} + +func (fake *ChaincodeStub) GetBindingReturns(result1 []byte, result2 error) { + fake.getBindingMutex.Lock() + defer fake.getBindingMutex.Unlock() + fake.GetBindingStub = nil + fake.getBindingReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetBindingReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getBindingMutex.Lock() + defer fake.getBindingMutex.Unlock() + fake.GetBindingStub = nil + if fake.getBindingReturnsOnCall == nil { + fake.getBindingReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getBindingReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetChannelID() string { + fake.getChannelIDMutex.Lock() + ret, specificReturn := fake.getChannelIDReturnsOnCall[len(fake.getChannelIDArgsForCall)] + fake.getChannelIDArgsForCall = append(fake.getChannelIDArgsForCall, struct { + }{}) + fake.recordInvocation("GetChannelID", []interface{}{}) + fake.getChannelIDMutex.Unlock() + if fake.GetChannelIDStub != nil { + return fake.GetChannelIDStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getChannelIDReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetChannelIDCallCount() int { + fake.getChannelIDMutex.RLock() + defer fake.getChannelIDMutex.RUnlock() + return len(fake.getChannelIDArgsForCall) +} + +func (fake *ChaincodeStub) GetChannelIDCalls(stub func() string) { + fake.getChannelIDMutex.Lock() + defer fake.getChannelIDMutex.Unlock() + fake.GetChannelIDStub = stub +} + +func (fake *ChaincodeStub) GetChannelIDReturns(result1 string) { + fake.getChannelIDMutex.Lock() + defer fake.getChannelIDMutex.Unlock() + fake.GetChannelIDStub = nil + fake.getChannelIDReturns = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetChannelIDReturnsOnCall(i int, result1 string) { + fake.getChannelIDMutex.Lock() + defer fake.getChannelIDMutex.Unlock() + fake.GetChannelIDStub = nil + if fake.getChannelIDReturnsOnCall == nil { + fake.getChannelIDReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getChannelIDReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetCreator() ([]byte, error) { + fake.getCreatorMutex.Lock() + ret, specificReturn := fake.getCreatorReturnsOnCall[len(fake.getCreatorArgsForCall)] + fake.getCreatorArgsForCall = append(fake.getCreatorArgsForCall, struct { + }{}) + fake.recordInvocation("GetCreator", []interface{}{}) + fake.getCreatorMutex.Unlock() + if fake.GetCreatorStub != nil { + return fake.GetCreatorStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getCreatorReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetCreatorCallCount() int { + fake.getCreatorMutex.RLock() + defer fake.getCreatorMutex.RUnlock() + return len(fake.getCreatorArgsForCall) +} + +func (fake *ChaincodeStub) GetCreatorCalls(stub func() ([]byte, error)) { + fake.getCreatorMutex.Lock() + defer fake.getCreatorMutex.Unlock() + fake.GetCreatorStub = stub +} + +func (fake *ChaincodeStub) GetCreatorReturns(result1 []byte, result2 error) { + fake.getCreatorMutex.Lock() + defer fake.getCreatorMutex.Unlock() + fake.GetCreatorStub = nil + fake.getCreatorReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetCreatorReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getCreatorMutex.Lock() + defer fake.getCreatorMutex.Unlock() + fake.GetCreatorStub = nil + if fake.getCreatorReturnsOnCall == nil { + fake.getCreatorReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getCreatorReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetDecorations() map[string][]byte { + fake.getDecorationsMutex.Lock() + ret, specificReturn := fake.getDecorationsReturnsOnCall[len(fake.getDecorationsArgsForCall)] + fake.getDecorationsArgsForCall = append(fake.getDecorationsArgsForCall, struct { + }{}) + fake.recordInvocation("GetDecorations", []interface{}{}) + fake.getDecorationsMutex.Unlock() + if fake.GetDecorationsStub != nil { + return fake.GetDecorationsStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getDecorationsReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetDecorationsCallCount() int { + fake.getDecorationsMutex.RLock() + defer fake.getDecorationsMutex.RUnlock() + return len(fake.getDecorationsArgsForCall) +} + +func (fake *ChaincodeStub) GetDecorationsCalls(stub func() map[string][]byte) { + fake.getDecorationsMutex.Lock() + defer fake.getDecorationsMutex.Unlock() + fake.GetDecorationsStub = stub +} + +func (fake *ChaincodeStub) GetDecorationsReturns(result1 map[string][]byte) { + fake.getDecorationsMutex.Lock() + defer fake.getDecorationsMutex.Unlock() + fake.GetDecorationsStub = nil + fake.getDecorationsReturns = struct { + result1 map[string][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetDecorationsReturnsOnCall(i int, result1 map[string][]byte) { + fake.getDecorationsMutex.Lock() + defer fake.getDecorationsMutex.Unlock() + fake.GetDecorationsStub = nil + if fake.getDecorationsReturnsOnCall == nil { + fake.getDecorationsReturnsOnCall = make(map[int]struct { + result1 map[string][]byte + }) + } + fake.getDecorationsReturnsOnCall[i] = struct { + result1 map[string][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetFunctionAndParameters() (string, []string) { + fake.getFunctionAndParametersMutex.Lock() + ret, specificReturn := fake.getFunctionAndParametersReturnsOnCall[len(fake.getFunctionAndParametersArgsForCall)] + fake.getFunctionAndParametersArgsForCall = append(fake.getFunctionAndParametersArgsForCall, struct { + }{}) + fake.recordInvocation("GetFunctionAndParameters", []interface{}{}) + fake.getFunctionAndParametersMutex.Unlock() + if fake.GetFunctionAndParametersStub != nil { + return fake.GetFunctionAndParametersStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getFunctionAndParametersReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetFunctionAndParametersCallCount() int { + fake.getFunctionAndParametersMutex.RLock() + defer fake.getFunctionAndParametersMutex.RUnlock() + return len(fake.getFunctionAndParametersArgsForCall) +} + +func (fake *ChaincodeStub) GetFunctionAndParametersCalls(stub func() (string, []string)) { + fake.getFunctionAndParametersMutex.Lock() + defer fake.getFunctionAndParametersMutex.Unlock() + fake.GetFunctionAndParametersStub = stub +} + +func (fake *ChaincodeStub) GetFunctionAndParametersReturns(result1 string, result2 []string) { + fake.getFunctionAndParametersMutex.Lock() + defer fake.getFunctionAndParametersMutex.Unlock() + fake.GetFunctionAndParametersStub = nil + fake.getFunctionAndParametersReturns = struct { + result1 string + result2 []string + }{result1, result2} +} + +func (fake *ChaincodeStub) GetFunctionAndParametersReturnsOnCall(i int, result1 string, result2 []string) { + fake.getFunctionAndParametersMutex.Lock() + defer fake.getFunctionAndParametersMutex.Unlock() + fake.GetFunctionAndParametersStub = nil + if fake.getFunctionAndParametersReturnsOnCall == nil { + fake.getFunctionAndParametersReturnsOnCall = make(map[int]struct { + result1 string + result2 []string + }) + } + fake.getFunctionAndParametersReturnsOnCall[i] = struct { + result1 string + result2 []string + }{result1, result2} +} + +func (fake *ChaincodeStub) GetHistoryForKey(arg1 string) (shim.HistoryQueryIteratorInterface, error) { + fake.getHistoryForKeyMutex.Lock() + ret, specificReturn := fake.getHistoryForKeyReturnsOnCall[len(fake.getHistoryForKeyArgsForCall)] + fake.getHistoryForKeyArgsForCall = append(fake.getHistoryForKeyArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetHistoryForKey", []interface{}{arg1}) + fake.getHistoryForKeyMutex.Unlock() + if fake.GetHistoryForKeyStub != nil { + return fake.GetHistoryForKeyStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getHistoryForKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetHistoryForKeyCallCount() int { + fake.getHistoryForKeyMutex.RLock() + defer fake.getHistoryForKeyMutex.RUnlock() + return len(fake.getHistoryForKeyArgsForCall) +} + +func (fake *ChaincodeStub) GetHistoryForKeyCalls(stub func(string) (shim.HistoryQueryIteratorInterface, error)) { + fake.getHistoryForKeyMutex.Lock() + defer fake.getHistoryForKeyMutex.Unlock() + fake.GetHistoryForKeyStub = stub +} + +func (fake *ChaincodeStub) GetHistoryForKeyArgsForCall(i int) string { + fake.getHistoryForKeyMutex.RLock() + defer fake.getHistoryForKeyMutex.RUnlock() + argsForCall := fake.getHistoryForKeyArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetHistoryForKeyReturns(result1 shim.HistoryQueryIteratorInterface, result2 error) { + fake.getHistoryForKeyMutex.Lock() + defer fake.getHistoryForKeyMutex.Unlock() + fake.GetHistoryForKeyStub = nil + fake.getHistoryForKeyReturns = struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetHistoryForKeyReturnsOnCall(i int, result1 shim.HistoryQueryIteratorInterface, result2 error) { + fake.getHistoryForKeyMutex.Lock() + defer fake.getHistoryForKeyMutex.Unlock() + fake.GetHistoryForKeyStub = nil + if fake.getHistoryForKeyReturnsOnCall == nil { + fake.getHistoryForKeyReturnsOnCall = make(map[int]struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + }) + } + fake.getHistoryForKeyReturnsOnCall[i] = struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateData(arg1 string, arg2 string) ([]byte, error) { + fake.getPrivateDataMutex.Lock() + ret, specificReturn := fake.getPrivateDataReturnsOnCall[len(fake.getPrivateDataArgsForCall)] + fake.getPrivateDataArgsForCall = append(fake.getPrivateDataArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateData", []interface{}{arg1, arg2}) + fake.getPrivateDataMutex.Unlock() + if fake.GetPrivateDataStub != nil { + return fake.GetPrivateDataStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataCallCount() int { + fake.getPrivateDataMutex.RLock() + defer fake.getPrivateDataMutex.RUnlock() + return len(fake.getPrivateDataArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataCalls(stub func(string, string) ([]byte, error)) { + fake.getPrivateDataMutex.Lock() + defer fake.getPrivateDataMutex.Unlock() + fake.GetPrivateDataStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataArgsForCall(i int) (string, string) { + fake.getPrivateDataMutex.RLock() + defer fake.getPrivateDataMutex.RUnlock() + argsForCall := fake.getPrivateDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataReturns(result1 []byte, result2 error) { + fake.getPrivateDataMutex.Lock() + defer fake.getPrivateDataMutex.Unlock() + fake.GetPrivateDataStub = nil + fake.getPrivateDataReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getPrivateDataMutex.Lock() + defer fake.getPrivateDataMutex.Unlock() + fake.GetPrivateDataStub = nil + if fake.getPrivateDataReturnsOnCall == nil { + fake.getPrivateDataReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getPrivateDataReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKey(arg1 string, arg2 string, arg3 []string) (shim.StateQueryIteratorInterface, error) { + var arg3Copy []string + if arg3 != nil { + arg3Copy = make([]string, len(arg3)) + copy(arg3Copy, arg3) + } + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + ret, specificReturn := fake.getPrivateDataByPartialCompositeKeyReturnsOnCall[len(fake.getPrivateDataByPartialCompositeKeyArgsForCall)] + fake.getPrivateDataByPartialCompositeKeyArgsForCall = append(fake.getPrivateDataByPartialCompositeKeyArgsForCall, struct { + arg1 string + arg2 string + arg3 []string + }{arg1, arg2, arg3Copy}) + fake.recordInvocation("GetPrivateDataByPartialCompositeKey", []interface{}{arg1, arg2, arg3Copy}) + fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + if fake.GetPrivateDataByPartialCompositeKeyStub != nil { + return fake.GetPrivateDataByPartialCompositeKeyStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataByPartialCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyCallCount() int { + fake.getPrivateDataByPartialCompositeKeyMutex.RLock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.RUnlock() + return len(fake.getPrivateDataByPartialCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyCalls(stub func(string, string, []string) (shim.StateQueryIteratorInterface, error)) { + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + fake.GetPrivateDataByPartialCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyArgsForCall(i int) (string, string, []string) { + fake.getPrivateDataByPartialCompositeKeyMutex.RLock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.RUnlock() + argsForCall := fake.getPrivateDataByPartialCompositeKeyArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + fake.GetPrivateDataByPartialCompositeKeyStub = nil + fake.getPrivateDataByPartialCompositeKeyReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + fake.GetPrivateDataByPartialCompositeKeyStub = nil + if fake.getPrivateDataByPartialCompositeKeyReturnsOnCall == nil { + fake.getPrivateDataByPartialCompositeKeyReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getPrivateDataByPartialCompositeKeyReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByRange(arg1 string, arg2 string, arg3 string) (shim.StateQueryIteratorInterface, error) { + fake.getPrivateDataByRangeMutex.Lock() + ret, specificReturn := fake.getPrivateDataByRangeReturnsOnCall[len(fake.getPrivateDataByRangeArgsForCall)] + fake.getPrivateDataByRangeArgsForCall = append(fake.getPrivateDataByRangeArgsForCall, struct { + arg1 string + arg2 string + arg3 string + }{arg1, arg2, arg3}) + fake.recordInvocation("GetPrivateDataByRange", []interface{}{arg1, arg2, arg3}) + fake.getPrivateDataByRangeMutex.Unlock() + if fake.GetPrivateDataByRangeStub != nil { + return fake.GetPrivateDataByRangeStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataByRangeReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeCallCount() int { + fake.getPrivateDataByRangeMutex.RLock() + defer fake.getPrivateDataByRangeMutex.RUnlock() + return len(fake.getPrivateDataByRangeArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeCalls(stub func(string, string, string) (shim.StateQueryIteratorInterface, error)) { + fake.getPrivateDataByRangeMutex.Lock() + defer fake.getPrivateDataByRangeMutex.Unlock() + fake.GetPrivateDataByRangeStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeArgsForCall(i int) (string, string, string) { + fake.getPrivateDataByRangeMutex.RLock() + defer fake.getPrivateDataByRangeMutex.RUnlock() + argsForCall := fake.getPrivateDataByRangeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByRangeMutex.Lock() + defer fake.getPrivateDataByRangeMutex.Unlock() + fake.GetPrivateDataByRangeStub = nil + fake.getPrivateDataByRangeReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByRangeMutex.Lock() + defer fake.getPrivateDataByRangeMutex.Unlock() + fake.GetPrivateDataByRangeStub = nil + if fake.getPrivateDataByRangeReturnsOnCall == nil { + fake.getPrivateDataByRangeReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getPrivateDataByRangeReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataHash(arg1 string, arg2 string) ([]byte, error) { + fake.getPrivateDataHashMutex.Lock() + ret, specificReturn := fake.getPrivateDataHashReturnsOnCall[len(fake.getPrivateDataHashArgsForCall)] + fake.getPrivateDataHashArgsForCall = append(fake.getPrivateDataHashArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateDataHash", []interface{}{arg1, arg2}) + fake.getPrivateDataHashMutex.Unlock() + if fake.GetPrivateDataHashStub != nil { + return fake.GetPrivateDataHashStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataHashReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataHashCallCount() int { + fake.getPrivateDataHashMutex.RLock() + defer fake.getPrivateDataHashMutex.RUnlock() + return len(fake.getPrivateDataHashArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataHashCalls(stub func(string, string) ([]byte, error)) { + fake.getPrivateDataHashMutex.Lock() + defer fake.getPrivateDataHashMutex.Unlock() + fake.GetPrivateDataHashStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataHashArgsForCall(i int) (string, string) { + fake.getPrivateDataHashMutex.RLock() + defer fake.getPrivateDataHashMutex.RUnlock() + argsForCall := fake.getPrivateDataHashArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataHashReturns(result1 []byte, result2 error) { + fake.getPrivateDataHashMutex.Lock() + defer fake.getPrivateDataHashMutex.Unlock() + fake.GetPrivateDataHashStub = nil + fake.getPrivateDataHashReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataHashReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getPrivateDataHashMutex.Lock() + defer fake.getPrivateDataHashMutex.Unlock() + fake.GetPrivateDataHashStub = nil + if fake.getPrivateDataHashReturnsOnCall == nil { + fake.getPrivateDataHashReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getPrivateDataHashReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResult(arg1 string, arg2 string) (shim.StateQueryIteratorInterface, error) { + fake.getPrivateDataQueryResultMutex.Lock() + ret, specificReturn := fake.getPrivateDataQueryResultReturnsOnCall[len(fake.getPrivateDataQueryResultArgsForCall)] + fake.getPrivateDataQueryResultArgsForCall = append(fake.getPrivateDataQueryResultArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateDataQueryResult", []interface{}{arg1, arg2}) + fake.getPrivateDataQueryResultMutex.Unlock() + if fake.GetPrivateDataQueryResultStub != nil { + return fake.GetPrivateDataQueryResultStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataQueryResultReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultCallCount() int { + fake.getPrivateDataQueryResultMutex.RLock() + defer fake.getPrivateDataQueryResultMutex.RUnlock() + return len(fake.getPrivateDataQueryResultArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultCalls(stub func(string, string) (shim.StateQueryIteratorInterface, error)) { + fake.getPrivateDataQueryResultMutex.Lock() + defer fake.getPrivateDataQueryResultMutex.Unlock() + fake.GetPrivateDataQueryResultStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultArgsForCall(i int) (string, string) { + fake.getPrivateDataQueryResultMutex.RLock() + defer fake.getPrivateDataQueryResultMutex.RUnlock() + argsForCall := fake.getPrivateDataQueryResultArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataQueryResultMutex.Lock() + defer fake.getPrivateDataQueryResultMutex.Unlock() + fake.GetPrivateDataQueryResultStub = nil + fake.getPrivateDataQueryResultReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataQueryResultMutex.Lock() + defer fake.getPrivateDataQueryResultMutex.Unlock() + fake.GetPrivateDataQueryResultStub = nil + if fake.getPrivateDataQueryResultReturnsOnCall == nil { + fake.getPrivateDataQueryResultReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getPrivateDataQueryResultReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameter(arg1 string, arg2 string) ([]byte, error) { + fake.getPrivateDataValidationParameterMutex.Lock() + ret, specificReturn := fake.getPrivateDataValidationParameterReturnsOnCall[len(fake.getPrivateDataValidationParameterArgsForCall)] + fake.getPrivateDataValidationParameterArgsForCall = append(fake.getPrivateDataValidationParameterArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateDataValidationParameter", []interface{}{arg1, arg2}) + fake.getPrivateDataValidationParameterMutex.Unlock() + if fake.GetPrivateDataValidationParameterStub != nil { + return fake.GetPrivateDataValidationParameterStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataValidationParameterReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterCallCount() int { + fake.getPrivateDataValidationParameterMutex.RLock() + defer fake.getPrivateDataValidationParameterMutex.RUnlock() + return len(fake.getPrivateDataValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterCalls(stub func(string, string) ([]byte, error)) { + fake.getPrivateDataValidationParameterMutex.Lock() + defer fake.getPrivateDataValidationParameterMutex.Unlock() + fake.GetPrivateDataValidationParameterStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterArgsForCall(i int) (string, string) { + fake.getPrivateDataValidationParameterMutex.RLock() + defer fake.getPrivateDataValidationParameterMutex.RUnlock() + argsForCall := fake.getPrivateDataValidationParameterArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterReturns(result1 []byte, result2 error) { + fake.getPrivateDataValidationParameterMutex.Lock() + defer fake.getPrivateDataValidationParameterMutex.Unlock() + fake.GetPrivateDataValidationParameterStub = nil + fake.getPrivateDataValidationParameterReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getPrivateDataValidationParameterMutex.Lock() + defer fake.getPrivateDataValidationParameterMutex.Unlock() + fake.GetPrivateDataValidationParameterStub = nil + if fake.getPrivateDataValidationParameterReturnsOnCall == nil { + fake.getPrivateDataValidationParameterReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getPrivateDataValidationParameterReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetQueryResult(arg1 string) (shim.StateQueryIteratorInterface, error) { + fake.getQueryResultMutex.Lock() + ret, specificReturn := fake.getQueryResultReturnsOnCall[len(fake.getQueryResultArgsForCall)] + fake.getQueryResultArgsForCall = append(fake.getQueryResultArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetQueryResult", []interface{}{arg1}) + fake.getQueryResultMutex.Unlock() + if fake.GetQueryResultStub != nil { + return fake.GetQueryResultStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getQueryResultReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetQueryResultCallCount() int { + fake.getQueryResultMutex.RLock() + defer fake.getQueryResultMutex.RUnlock() + return len(fake.getQueryResultArgsForCall) +} + +func (fake *ChaincodeStub) GetQueryResultCalls(stub func(string) (shim.StateQueryIteratorInterface, error)) { + fake.getQueryResultMutex.Lock() + defer fake.getQueryResultMutex.Unlock() + fake.GetQueryResultStub = stub +} + +func (fake *ChaincodeStub) GetQueryResultArgsForCall(i int) string { + fake.getQueryResultMutex.RLock() + defer fake.getQueryResultMutex.RUnlock() + argsForCall := fake.getQueryResultArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetQueryResultReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getQueryResultMutex.Lock() + defer fake.getQueryResultMutex.Unlock() + fake.GetQueryResultStub = nil + fake.getQueryResultReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetQueryResultReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getQueryResultMutex.Lock() + defer fake.getQueryResultMutex.Unlock() + fake.GetQueryResultStub = nil + if fake.getQueryResultReturnsOnCall == nil { + fake.getQueryResultReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getQueryResultReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetQueryResultWithPagination(arg1 string, arg2 int32, arg3 string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + fake.getQueryResultWithPaginationMutex.Lock() + ret, specificReturn := fake.getQueryResultWithPaginationReturnsOnCall[len(fake.getQueryResultWithPaginationArgsForCall)] + fake.getQueryResultWithPaginationArgsForCall = append(fake.getQueryResultWithPaginationArgsForCall, struct { + arg1 string + arg2 int32 + arg3 string + }{arg1, arg2, arg3}) + fake.recordInvocation("GetQueryResultWithPagination", []interface{}{arg1, arg2, arg3}) + fake.getQueryResultWithPaginationMutex.Unlock() + if fake.GetQueryResultWithPaginationStub != nil { + return fake.GetQueryResultWithPaginationStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getQueryResultWithPaginationReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationCallCount() int { + fake.getQueryResultWithPaginationMutex.RLock() + defer fake.getQueryResultWithPaginationMutex.RUnlock() + return len(fake.getQueryResultWithPaginationArgsForCall) +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationCalls(stub func(string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error)) { + fake.getQueryResultWithPaginationMutex.Lock() + defer fake.getQueryResultWithPaginationMutex.Unlock() + fake.GetQueryResultWithPaginationStub = stub +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationArgsForCall(i int) (string, int32, string) { + fake.getQueryResultWithPaginationMutex.RLock() + defer fake.getQueryResultWithPaginationMutex.RUnlock() + argsForCall := fake.getQueryResultWithPaginationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationReturns(result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getQueryResultWithPaginationMutex.Lock() + defer fake.getQueryResultWithPaginationMutex.Unlock() + fake.GetQueryResultWithPaginationStub = nil + fake.getQueryResultWithPaginationReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getQueryResultWithPaginationMutex.Lock() + defer fake.getQueryResultWithPaginationMutex.Unlock() + fake.GetQueryResultWithPaginationStub = nil + if fake.getQueryResultWithPaginationReturnsOnCall == nil { + fake.getQueryResultWithPaginationReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }) + } + fake.getQueryResultWithPaginationReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetSignedProposal() (*peer.SignedProposal, error) { + fake.getSignedProposalMutex.Lock() + ret, specificReturn := fake.getSignedProposalReturnsOnCall[len(fake.getSignedProposalArgsForCall)] + fake.getSignedProposalArgsForCall = append(fake.getSignedProposalArgsForCall, struct { + }{}) + fake.recordInvocation("GetSignedProposal", []interface{}{}) + fake.getSignedProposalMutex.Unlock() + if fake.GetSignedProposalStub != nil { + return fake.GetSignedProposalStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getSignedProposalReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetSignedProposalCallCount() int { + fake.getSignedProposalMutex.RLock() + defer fake.getSignedProposalMutex.RUnlock() + return len(fake.getSignedProposalArgsForCall) +} + +func (fake *ChaincodeStub) GetSignedProposalCalls(stub func() (*peer.SignedProposal, error)) { + fake.getSignedProposalMutex.Lock() + defer fake.getSignedProposalMutex.Unlock() + fake.GetSignedProposalStub = stub +} + +func (fake *ChaincodeStub) GetSignedProposalReturns(result1 *peer.SignedProposal, result2 error) { + fake.getSignedProposalMutex.Lock() + defer fake.getSignedProposalMutex.Unlock() + fake.GetSignedProposalStub = nil + fake.getSignedProposalReturns = struct { + result1 *peer.SignedProposal + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetSignedProposalReturnsOnCall(i int, result1 *peer.SignedProposal, result2 error) { + fake.getSignedProposalMutex.Lock() + defer fake.getSignedProposalMutex.Unlock() + fake.GetSignedProposalStub = nil + if fake.getSignedProposalReturnsOnCall == nil { + fake.getSignedProposalReturnsOnCall = make(map[int]struct { + result1 *peer.SignedProposal + result2 error + }) + } + fake.getSignedProposalReturnsOnCall[i] = struct { + result1 *peer.SignedProposal + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetState(arg1 string) ([]byte, error) { + fake.getStateMutex.Lock() + ret, specificReturn := fake.getStateReturnsOnCall[len(fake.getStateArgsForCall)] + fake.getStateArgsForCall = append(fake.getStateArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetState", []interface{}{arg1}) + fake.getStateMutex.Unlock() + if fake.GetStateStub != nil { + return fake.GetStateStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateCallCount() int { + fake.getStateMutex.RLock() + defer fake.getStateMutex.RUnlock() + return len(fake.getStateArgsForCall) +} + +func (fake *ChaincodeStub) GetStateCalls(stub func(string) ([]byte, error)) { + fake.getStateMutex.Lock() + defer fake.getStateMutex.Unlock() + fake.GetStateStub = stub +} + +func (fake *ChaincodeStub) GetStateArgsForCall(i int) string { + fake.getStateMutex.RLock() + defer fake.getStateMutex.RUnlock() + argsForCall := fake.getStateArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetStateReturns(result1 []byte, result2 error) { + fake.getStateMutex.Lock() + defer fake.getStateMutex.Unlock() + fake.GetStateStub = nil + fake.getStateReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getStateMutex.Lock() + defer fake.getStateMutex.Unlock() + fake.GetStateStub = nil + if fake.getStateReturnsOnCall == nil { + fake.getStateReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getStateReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKey(arg1 string, arg2 []string) (shim.StateQueryIteratorInterface, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + fake.getStateByPartialCompositeKeyMutex.Lock() + ret, specificReturn := fake.getStateByPartialCompositeKeyReturnsOnCall[len(fake.getStateByPartialCompositeKeyArgsForCall)] + fake.getStateByPartialCompositeKeyArgsForCall = append(fake.getStateByPartialCompositeKeyArgsForCall, struct { + arg1 string + arg2 []string + }{arg1, arg2Copy}) + fake.recordInvocation("GetStateByPartialCompositeKey", []interface{}{arg1, arg2Copy}) + fake.getStateByPartialCompositeKeyMutex.Unlock() + if fake.GetStateByPartialCompositeKeyStub != nil { + return fake.GetStateByPartialCompositeKeyStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateByPartialCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyCallCount() int { + fake.getStateByPartialCompositeKeyMutex.RLock() + defer fake.getStateByPartialCompositeKeyMutex.RUnlock() + return len(fake.getStateByPartialCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyCalls(stub func(string, []string) (shim.StateQueryIteratorInterface, error)) { + fake.getStateByPartialCompositeKeyMutex.Lock() + defer fake.getStateByPartialCompositeKeyMutex.Unlock() + fake.GetStateByPartialCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyArgsForCall(i int) (string, []string) { + fake.getStateByPartialCompositeKeyMutex.RLock() + defer fake.getStateByPartialCompositeKeyMutex.RUnlock() + argsForCall := fake.getStateByPartialCompositeKeyArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByPartialCompositeKeyMutex.Lock() + defer fake.getStateByPartialCompositeKeyMutex.Unlock() + fake.GetStateByPartialCompositeKeyStub = nil + fake.getStateByPartialCompositeKeyReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByPartialCompositeKeyMutex.Lock() + defer fake.getStateByPartialCompositeKeyMutex.Unlock() + fake.GetStateByPartialCompositeKeyStub = nil + if fake.getStateByPartialCompositeKeyReturnsOnCall == nil { + fake.getStateByPartialCompositeKeyReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getStateByPartialCompositeKeyReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPagination(arg1 string, arg2 []string, arg3 int32, arg4 string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + ret, specificReturn := fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall[len(fake.getStateByPartialCompositeKeyWithPaginationArgsForCall)] + fake.getStateByPartialCompositeKeyWithPaginationArgsForCall = append(fake.getStateByPartialCompositeKeyWithPaginationArgsForCall, struct { + arg1 string + arg2 []string + arg3 int32 + arg4 string + }{arg1, arg2Copy, arg3, arg4}) + fake.recordInvocation("GetStateByPartialCompositeKeyWithPagination", []interface{}{arg1, arg2Copy, arg3, arg4}) + fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + if fake.GetStateByPartialCompositeKeyWithPaginationStub != nil { + return fake.GetStateByPartialCompositeKeyWithPaginationStub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getStateByPartialCompositeKeyWithPaginationReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationCallCount() int { + fake.getStateByPartialCompositeKeyWithPaginationMutex.RLock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.RUnlock() + return len(fake.getStateByPartialCompositeKeyWithPaginationArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationCalls(stub func(string, []string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error)) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + fake.GetStateByPartialCompositeKeyWithPaginationStub = stub +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationArgsForCall(i int) (string, []string, int32, string) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.RLock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.RUnlock() + argsForCall := fake.getStateByPartialCompositeKeyWithPaginationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationReturns(result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + fake.GetStateByPartialCompositeKeyWithPaginationStub = nil + fake.getStateByPartialCompositeKeyWithPaginationReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + fake.GetStateByPartialCompositeKeyWithPaginationStub = nil + if fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall == nil { + fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }) + } + fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateByRange(arg1 string, arg2 string) (shim.StateQueryIteratorInterface, error) { + fake.getStateByRangeMutex.Lock() + ret, specificReturn := fake.getStateByRangeReturnsOnCall[len(fake.getStateByRangeArgsForCall)] + fake.getStateByRangeArgsForCall = append(fake.getStateByRangeArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetStateByRange", []interface{}{arg1, arg2}) + fake.getStateByRangeMutex.Unlock() + if fake.GetStateByRangeStub != nil { + return fake.GetStateByRangeStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateByRangeReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateByRangeCallCount() int { + fake.getStateByRangeMutex.RLock() + defer fake.getStateByRangeMutex.RUnlock() + return len(fake.getStateByRangeArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByRangeCalls(stub func(string, string) (shim.StateQueryIteratorInterface, error)) { + fake.getStateByRangeMutex.Lock() + defer fake.getStateByRangeMutex.Unlock() + fake.GetStateByRangeStub = stub +} + +func (fake *ChaincodeStub) GetStateByRangeArgsForCall(i int) (string, string) { + fake.getStateByRangeMutex.RLock() + defer fake.getStateByRangeMutex.RUnlock() + argsForCall := fake.getStateByRangeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetStateByRangeReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByRangeMutex.Lock() + defer fake.getStateByRangeMutex.Unlock() + fake.GetStateByRangeStub = nil + fake.getStateByRangeReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByRangeReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByRangeMutex.Lock() + defer fake.getStateByRangeMutex.Unlock() + fake.GetStateByRangeStub = nil + if fake.getStateByRangeReturnsOnCall == nil { + fake.getStateByRangeReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getStateByRangeReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByRangeWithPagination(arg1 string, arg2 string, arg3 int32, arg4 string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + fake.getStateByRangeWithPaginationMutex.Lock() + ret, specificReturn := fake.getStateByRangeWithPaginationReturnsOnCall[len(fake.getStateByRangeWithPaginationArgsForCall)] + fake.getStateByRangeWithPaginationArgsForCall = append(fake.getStateByRangeWithPaginationArgsForCall, struct { + arg1 string + arg2 string + arg3 int32 + arg4 string + }{arg1, arg2, arg3, arg4}) + fake.recordInvocation("GetStateByRangeWithPagination", []interface{}{arg1, arg2, arg3, arg4}) + fake.getStateByRangeWithPaginationMutex.Unlock() + if fake.GetStateByRangeWithPaginationStub != nil { + return fake.GetStateByRangeWithPaginationStub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getStateByRangeWithPaginationReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationCallCount() int { + fake.getStateByRangeWithPaginationMutex.RLock() + defer fake.getStateByRangeWithPaginationMutex.RUnlock() + return len(fake.getStateByRangeWithPaginationArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationCalls(stub func(string, string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error)) { + fake.getStateByRangeWithPaginationMutex.Lock() + defer fake.getStateByRangeWithPaginationMutex.Unlock() + fake.GetStateByRangeWithPaginationStub = stub +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationArgsForCall(i int) (string, string, int32, string) { + fake.getStateByRangeWithPaginationMutex.RLock() + defer fake.getStateByRangeWithPaginationMutex.RUnlock() + argsForCall := fake.getStateByRangeWithPaginationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationReturns(result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByRangeWithPaginationMutex.Lock() + defer fake.getStateByRangeWithPaginationMutex.Unlock() + fake.GetStateByRangeWithPaginationStub = nil + fake.getStateByRangeWithPaginationReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByRangeWithPaginationMutex.Lock() + defer fake.getStateByRangeWithPaginationMutex.Unlock() + fake.GetStateByRangeWithPaginationStub = nil + if fake.getStateByRangeWithPaginationReturnsOnCall == nil { + fake.getStateByRangeWithPaginationReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }) + } + fake.getStateByRangeWithPaginationReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateValidationParameter(arg1 string) ([]byte, error) { + fake.getStateValidationParameterMutex.Lock() + ret, specificReturn := fake.getStateValidationParameterReturnsOnCall[len(fake.getStateValidationParameterArgsForCall)] + fake.getStateValidationParameterArgsForCall = append(fake.getStateValidationParameterArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetStateValidationParameter", []interface{}{arg1}) + fake.getStateValidationParameterMutex.Unlock() + if fake.GetStateValidationParameterStub != nil { + return fake.GetStateValidationParameterStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateValidationParameterReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateValidationParameterCallCount() int { + fake.getStateValidationParameterMutex.RLock() + defer fake.getStateValidationParameterMutex.RUnlock() + return len(fake.getStateValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) GetStateValidationParameterCalls(stub func(string) ([]byte, error)) { + fake.getStateValidationParameterMutex.Lock() + defer fake.getStateValidationParameterMutex.Unlock() + fake.GetStateValidationParameterStub = stub +} + +func (fake *ChaincodeStub) GetStateValidationParameterArgsForCall(i int) string { + fake.getStateValidationParameterMutex.RLock() + defer fake.getStateValidationParameterMutex.RUnlock() + argsForCall := fake.getStateValidationParameterArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetStateValidationParameterReturns(result1 []byte, result2 error) { + fake.getStateValidationParameterMutex.Lock() + defer fake.getStateValidationParameterMutex.Unlock() + fake.GetStateValidationParameterStub = nil + fake.getStateValidationParameterReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateValidationParameterReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getStateValidationParameterMutex.Lock() + defer fake.getStateValidationParameterMutex.Unlock() + fake.GetStateValidationParameterStub = nil + if fake.getStateValidationParameterReturnsOnCall == nil { + fake.getStateValidationParameterReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getStateValidationParameterReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStringArgs() []string { + fake.getStringArgsMutex.Lock() + ret, specificReturn := fake.getStringArgsReturnsOnCall[len(fake.getStringArgsArgsForCall)] + fake.getStringArgsArgsForCall = append(fake.getStringArgsArgsForCall, struct { + }{}) + fake.recordInvocation("GetStringArgs", []interface{}{}) + fake.getStringArgsMutex.Unlock() + if fake.GetStringArgsStub != nil { + return fake.GetStringArgsStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getStringArgsReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetStringArgsCallCount() int { + fake.getStringArgsMutex.RLock() + defer fake.getStringArgsMutex.RUnlock() + return len(fake.getStringArgsArgsForCall) +} + +func (fake *ChaincodeStub) GetStringArgsCalls(stub func() []string) { + fake.getStringArgsMutex.Lock() + defer fake.getStringArgsMutex.Unlock() + fake.GetStringArgsStub = stub +} + +func (fake *ChaincodeStub) GetStringArgsReturns(result1 []string) { + fake.getStringArgsMutex.Lock() + defer fake.getStringArgsMutex.Unlock() + fake.GetStringArgsStub = nil + fake.getStringArgsReturns = struct { + result1 []string + }{result1} +} + +func (fake *ChaincodeStub) GetStringArgsReturnsOnCall(i int, result1 []string) { + fake.getStringArgsMutex.Lock() + defer fake.getStringArgsMutex.Unlock() + fake.GetStringArgsStub = nil + if fake.getStringArgsReturnsOnCall == nil { + fake.getStringArgsReturnsOnCall = make(map[int]struct { + result1 []string + }) + } + fake.getStringArgsReturnsOnCall[i] = struct { + result1 []string + }{result1} +} + +func (fake *ChaincodeStub) GetTransient() (map[string][]byte, error) { + fake.getTransientMutex.Lock() + ret, specificReturn := fake.getTransientReturnsOnCall[len(fake.getTransientArgsForCall)] + fake.getTransientArgsForCall = append(fake.getTransientArgsForCall, struct { + }{}) + fake.recordInvocation("GetTransient", []interface{}{}) + fake.getTransientMutex.Unlock() + if fake.GetTransientStub != nil { + return fake.GetTransientStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getTransientReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetTransientCallCount() int { + fake.getTransientMutex.RLock() + defer fake.getTransientMutex.RUnlock() + return len(fake.getTransientArgsForCall) +} + +func (fake *ChaincodeStub) GetTransientCalls(stub func() (map[string][]byte, error)) { + fake.getTransientMutex.Lock() + defer fake.getTransientMutex.Unlock() + fake.GetTransientStub = stub +} + +func (fake *ChaincodeStub) GetTransientReturns(result1 map[string][]byte, result2 error) { + fake.getTransientMutex.Lock() + defer fake.getTransientMutex.Unlock() + fake.GetTransientStub = nil + fake.getTransientReturns = struct { + result1 map[string][]byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetTransientReturnsOnCall(i int, result1 map[string][]byte, result2 error) { + fake.getTransientMutex.Lock() + defer fake.getTransientMutex.Unlock() + fake.GetTransientStub = nil + if fake.getTransientReturnsOnCall == nil { + fake.getTransientReturnsOnCall = make(map[int]struct { + result1 map[string][]byte + result2 error + }) + } + fake.getTransientReturnsOnCall[i] = struct { + result1 map[string][]byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetTxID() string { + fake.getTxIDMutex.Lock() + ret, specificReturn := fake.getTxIDReturnsOnCall[len(fake.getTxIDArgsForCall)] + fake.getTxIDArgsForCall = append(fake.getTxIDArgsForCall, struct { + }{}) + fake.recordInvocation("GetTxID", []interface{}{}) + fake.getTxIDMutex.Unlock() + if fake.GetTxIDStub != nil { + return fake.GetTxIDStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getTxIDReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetTxIDCallCount() int { + fake.getTxIDMutex.RLock() + defer fake.getTxIDMutex.RUnlock() + return len(fake.getTxIDArgsForCall) +} + +func (fake *ChaincodeStub) GetTxIDCalls(stub func() string) { + fake.getTxIDMutex.Lock() + defer fake.getTxIDMutex.Unlock() + fake.GetTxIDStub = stub +} + +func (fake *ChaincodeStub) GetTxIDReturns(result1 string) { + fake.getTxIDMutex.Lock() + defer fake.getTxIDMutex.Unlock() + fake.GetTxIDStub = nil + fake.getTxIDReturns = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetTxIDReturnsOnCall(i int, result1 string) { + fake.getTxIDMutex.Lock() + defer fake.getTxIDMutex.Unlock() + fake.GetTxIDStub = nil + if fake.getTxIDReturnsOnCall == nil { + fake.getTxIDReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getTxIDReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetTxTimestamp() (*timestamp.Timestamp, error) { + fake.getTxTimestampMutex.Lock() + ret, specificReturn := fake.getTxTimestampReturnsOnCall[len(fake.getTxTimestampArgsForCall)] + fake.getTxTimestampArgsForCall = append(fake.getTxTimestampArgsForCall, struct { + }{}) + fake.recordInvocation("GetTxTimestamp", []interface{}{}) + fake.getTxTimestampMutex.Unlock() + if fake.GetTxTimestampStub != nil { + return fake.GetTxTimestampStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getTxTimestampReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetTxTimestampCallCount() int { + fake.getTxTimestampMutex.RLock() + defer fake.getTxTimestampMutex.RUnlock() + return len(fake.getTxTimestampArgsForCall) +} + +func (fake *ChaincodeStub) GetTxTimestampCalls(stub func() (*timestamp.Timestamp, error)) { + fake.getTxTimestampMutex.Lock() + defer fake.getTxTimestampMutex.Unlock() + fake.GetTxTimestampStub = stub +} + +func (fake *ChaincodeStub) GetTxTimestampReturns(result1 *timestamp.Timestamp, result2 error) { + fake.getTxTimestampMutex.Lock() + defer fake.getTxTimestampMutex.Unlock() + fake.GetTxTimestampStub = nil + fake.getTxTimestampReturns = struct { + result1 *timestamp.Timestamp + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetTxTimestampReturnsOnCall(i int, result1 *timestamp.Timestamp, result2 error) { + fake.getTxTimestampMutex.Lock() + defer fake.getTxTimestampMutex.Unlock() + fake.GetTxTimestampStub = nil + if fake.getTxTimestampReturnsOnCall == nil { + fake.getTxTimestampReturnsOnCall = make(map[int]struct { + result1 *timestamp.Timestamp + result2 error + }) + } + fake.getTxTimestampReturnsOnCall[i] = struct { + result1 *timestamp.Timestamp + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) InvokeChaincode(arg1 string, arg2 [][]byte, arg3 string) peer.Response { + var arg2Copy [][]byte + if arg2 != nil { + arg2Copy = make([][]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.invokeChaincodeMutex.Lock() + ret, specificReturn := fake.invokeChaincodeReturnsOnCall[len(fake.invokeChaincodeArgsForCall)] + fake.invokeChaincodeArgsForCall = append(fake.invokeChaincodeArgsForCall, struct { + arg1 string + arg2 [][]byte + arg3 string + }{arg1, arg2Copy, arg3}) + fake.recordInvocation("InvokeChaincode", []interface{}{arg1, arg2Copy, arg3}) + fake.invokeChaincodeMutex.Unlock() + if fake.InvokeChaincodeStub != nil { + return fake.InvokeChaincodeStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.invokeChaincodeReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) InvokeChaincodeCallCount() int { + fake.invokeChaincodeMutex.RLock() + defer fake.invokeChaincodeMutex.RUnlock() + return len(fake.invokeChaincodeArgsForCall) +} + +func (fake *ChaincodeStub) InvokeChaincodeCalls(stub func(string, [][]byte, string) peer.Response) { + fake.invokeChaincodeMutex.Lock() + defer fake.invokeChaincodeMutex.Unlock() + fake.InvokeChaincodeStub = stub +} + +func (fake *ChaincodeStub) InvokeChaincodeArgsForCall(i int) (string, [][]byte, string) { + fake.invokeChaincodeMutex.RLock() + defer fake.invokeChaincodeMutex.RUnlock() + argsForCall := fake.invokeChaincodeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) InvokeChaincodeReturns(result1 peer.Response) { + fake.invokeChaincodeMutex.Lock() + defer fake.invokeChaincodeMutex.Unlock() + fake.InvokeChaincodeStub = nil + fake.invokeChaincodeReturns = struct { + result1 peer.Response + }{result1} +} + +func (fake *ChaincodeStub) InvokeChaincodeReturnsOnCall(i int, result1 peer.Response) { + fake.invokeChaincodeMutex.Lock() + defer fake.invokeChaincodeMutex.Unlock() + fake.InvokeChaincodeStub = nil + if fake.invokeChaincodeReturnsOnCall == nil { + fake.invokeChaincodeReturnsOnCall = make(map[int]struct { + result1 peer.Response + }) + } + fake.invokeChaincodeReturnsOnCall[i] = struct { + result1 peer.Response + }{result1} +} + +func (fake *ChaincodeStub) PutPrivateData(arg1 string, arg2 string, arg3 []byte) error { + var arg3Copy []byte + if arg3 != nil { + arg3Copy = make([]byte, len(arg3)) + copy(arg3Copy, arg3) + } + fake.putPrivateDataMutex.Lock() + ret, specificReturn := fake.putPrivateDataReturnsOnCall[len(fake.putPrivateDataArgsForCall)] + fake.putPrivateDataArgsForCall = append(fake.putPrivateDataArgsForCall, struct { + arg1 string + arg2 string + arg3 []byte + }{arg1, arg2, arg3Copy}) + fake.recordInvocation("PutPrivateData", []interface{}{arg1, arg2, arg3Copy}) + fake.putPrivateDataMutex.Unlock() + if fake.PutPrivateDataStub != nil { + return fake.PutPrivateDataStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.putPrivateDataReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) PutPrivateDataCallCount() int { + fake.putPrivateDataMutex.RLock() + defer fake.putPrivateDataMutex.RUnlock() + return len(fake.putPrivateDataArgsForCall) +} + +func (fake *ChaincodeStub) PutPrivateDataCalls(stub func(string, string, []byte) error) { + fake.putPrivateDataMutex.Lock() + defer fake.putPrivateDataMutex.Unlock() + fake.PutPrivateDataStub = stub +} + +func (fake *ChaincodeStub) PutPrivateDataArgsForCall(i int) (string, string, []byte) { + fake.putPrivateDataMutex.RLock() + defer fake.putPrivateDataMutex.RUnlock() + argsForCall := fake.putPrivateDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) PutPrivateDataReturns(result1 error) { + fake.putPrivateDataMutex.Lock() + defer fake.putPrivateDataMutex.Unlock() + fake.PutPrivateDataStub = nil + fake.putPrivateDataReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) PutPrivateDataReturnsOnCall(i int, result1 error) { + fake.putPrivateDataMutex.Lock() + defer fake.putPrivateDataMutex.Unlock() + fake.PutPrivateDataStub = nil + if fake.putPrivateDataReturnsOnCall == nil { + fake.putPrivateDataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.putPrivateDataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) PutState(arg1 string, arg2 []byte) error { + var arg2Copy []byte + if arg2 != nil { + arg2Copy = make([]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.putStateMutex.Lock() + ret, specificReturn := fake.putStateReturnsOnCall[len(fake.putStateArgsForCall)] + fake.putStateArgsForCall = append(fake.putStateArgsForCall, struct { + arg1 string + arg2 []byte + }{arg1, arg2Copy}) + fake.recordInvocation("PutState", []interface{}{arg1, arg2Copy}) + fake.putStateMutex.Unlock() + if fake.PutStateStub != nil { + return fake.PutStateStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.putStateReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) PutStateCallCount() int { + fake.putStateMutex.RLock() + defer fake.putStateMutex.RUnlock() + return len(fake.putStateArgsForCall) +} + +func (fake *ChaincodeStub) PutStateCalls(stub func(string, []byte) error) { + fake.putStateMutex.Lock() + defer fake.putStateMutex.Unlock() + fake.PutStateStub = stub +} + +func (fake *ChaincodeStub) PutStateArgsForCall(i int) (string, []byte) { + fake.putStateMutex.RLock() + defer fake.putStateMutex.RUnlock() + argsForCall := fake.putStateArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) PutStateReturns(result1 error) { + fake.putStateMutex.Lock() + defer fake.putStateMutex.Unlock() + fake.PutStateStub = nil + fake.putStateReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) PutStateReturnsOnCall(i int, result1 error) { + fake.putStateMutex.Lock() + defer fake.putStateMutex.Unlock() + fake.PutStateStub = nil + if fake.putStateReturnsOnCall == nil { + fake.putStateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.putStateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetEvent(arg1 string, arg2 []byte) error { + var arg2Copy []byte + if arg2 != nil { + arg2Copy = make([]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.setEventMutex.Lock() + ret, specificReturn := fake.setEventReturnsOnCall[len(fake.setEventArgsForCall)] + fake.setEventArgsForCall = append(fake.setEventArgsForCall, struct { + arg1 string + arg2 []byte + }{arg1, arg2Copy}) + fake.recordInvocation("SetEvent", []interface{}{arg1, arg2Copy}) + fake.setEventMutex.Unlock() + if fake.SetEventStub != nil { + return fake.SetEventStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setEventReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) SetEventCallCount() int { + fake.setEventMutex.RLock() + defer fake.setEventMutex.RUnlock() + return len(fake.setEventArgsForCall) +} + +func (fake *ChaincodeStub) SetEventCalls(stub func(string, []byte) error) { + fake.setEventMutex.Lock() + defer fake.setEventMutex.Unlock() + fake.SetEventStub = stub +} + +func (fake *ChaincodeStub) SetEventArgsForCall(i int) (string, []byte) { + fake.setEventMutex.RLock() + defer fake.setEventMutex.RUnlock() + argsForCall := fake.setEventArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) SetEventReturns(result1 error) { + fake.setEventMutex.Lock() + defer fake.setEventMutex.Unlock() + fake.SetEventStub = nil + fake.setEventReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetEventReturnsOnCall(i int, result1 error) { + fake.setEventMutex.Lock() + defer fake.setEventMutex.Unlock() + fake.SetEventStub = nil + if fake.setEventReturnsOnCall == nil { + fake.setEventReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setEventReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameter(arg1 string, arg2 string, arg3 []byte) error { + var arg3Copy []byte + if arg3 != nil { + arg3Copy = make([]byte, len(arg3)) + copy(arg3Copy, arg3) + } + fake.setPrivateDataValidationParameterMutex.Lock() + ret, specificReturn := fake.setPrivateDataValidationParameterReturnsOnCall[len(fake.setPrivateDataValidationParameterArgsForCall)] + fake.setPrivateDataValidationParameterArgsForCall = append(fake.setPrivateDataValidationParameterArgsForCall, struct { + arg1 string + arg2 string + arg3 []byte + }{arg1, arg2, arg3Copy}) + fake.recordInvocation("SetPrivateDataValidationParameter", []interface{}{arg1, arg2, arg3Copy}) + fake.setPrivateDataValidationParameterMutex.Unlock() + if fake.SetPrivateDataValidationParameterStub != nil { + return fake.SetPrivateDataValidationParameterStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setPrivateDataValidationParameterReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterCallCount() int { + fake.setPrivateDataValidationParameterMutex.RLock() + defer fake.setPrivateDataValidationParameterMutex.RUnlock() + return len(fake.setPrivateDataValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterCalls(stub func(string, string, []byte) error) { + fake.setPrivateDataValidationParameterMutex.Lock() + defer fake.setPrivateDataValidationParameterMutex.Unlock() + fake.SetPrivateDataValidationParameterStub = stub +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterArgsForCall(i int) (string, string, []byte) { + fake.setPrivateDataValidationParameterMutex.RLock() + defer fake.setPrivateDataValidationParameterMutex.RUnlock() + argsForCall := fake.setPrivateDataValidationParameterArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterReturns(result1 error) { + fake.setPrivateDataValidationParameterMutex.Lock() + defer fake.setPrivateDataValidationParameterMutex.Unlock() + fake.SetPrivateDataValidationParameterStub = nil + fake.setPrivateDataValidationParameterReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterReturnsOnCall(i int, result1 error) { + fake.setPrivateDataValidationParameterMutex.Lock() + defer fake.setPrivateDataValidationParameterMutex.Unlock() + fake.SetPrivateDataValidationParameterStub = nil + if fake.setPrivateDataValidationParameterReturnsOnCall == nil { + fake.setPrivateDataValidationParameterReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setPrivateDataValidationParameterReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetStateValidationParameter(arg1 string, arg2 []byte) error { + var arg2Copy []byte + if arg2 != nil { + arg2Copy = make([]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.setStateValidationParameterMutex.Lock() + ret, specificReturn := fake.setStateValidationParameterReturnsOnCall[len(fake.setStateValidationParameterArgsForCall)] + fake.setStateValidationParameterArgsForCall = append(fake.setStateValidationParameterArgsForCall, struct { + arg1 string + arg2 []byte + }{arg1, arg2Copy}) + fake.recordInvocation("SetStateValidationParameter", []interface{}{arg1, arg2Copy}) + fake.setStateValidationParameterMutex.Unlock() + if fake.SetStateValidationParameterStub != nil { + return fake.SetStateValidationParameterStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setStateValidationParameterReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) SetStateValidationParameterCallCount() int { + fake.setStateValidationParameterMutex.RLock() + defer fake.setStateValidationParameterMutex.RUnlock() + return len(fake.setStateValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) SetStateValidationParameterCalls(stub func(string, []byte) error) { + fake.setStateValidationParameterMutex.Lock() + defer fake.setStateValidationParameterMutex.Unlock() + fake.SetStateValidationParameterStub = stub +} + +func (fake *ChaincodeStub) SetStateValidationParameterArgsForCall(i int) (string, []byte) { + fake.setStateValidationParameterMutex.RLock() + defer fake.setStateValidationParameterMutex.RUnlock() + argsForCall := fake.setStateValidationParameterArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) SetStateValidationParameterReturns(result1 error) { + fake.setStateValidationParameterMutex.Lock() + defer fake.setStateValidationParameterMutex.Unlock() + fake.SetStateValidationParameterStub = nil + fake.setStateValidationParameterReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetStateValidationParameterReturnsOnCall(i int, result1 error) { + fake.setStateValidationParameterMutex.Lock() + defer fake.setStateValidationParameterMutex.Unlock() + fake.SetStateValidationParameterStub = nil + if fake.setStateValidationParameterReturnsOnCall == nil { + fake.setStateValidationParameterReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setStateValidationParameterReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SplitCompositeKey(arg1 string) (string, []string, error) { + fake.splitCompositeKeyMutex.Lock() + ret, specificReturn := fake.splitCompositeKeyReturnsOnCall[len(fake.splitCompositeKeyArgsForCall)] + fake.splitCompositeKeyArgsForCall = append(fake.splitCompositeKeyArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("SplitCompositeKey", []interface{}{arg1}) + fake.splitCompositeKeyMutex.Unlock() + if fake.SplitCompositeKeyStub != nil { + return fake.SplitCompositeKeyStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.splitCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) SplitCompositeKeyCallCount() int { + fake.splitCompositeKeyMutex.RLock() + defer fake.splitCompositeKeyMutex.RUnlock() + return len(fake.splitCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) SplitCompositeKeyCalls(stub func(string) (string, []string, error)) { + fake.splitCompositeKeyMutex.Lock() + defer fake.splitCompositeKeyMutex.Unlock() + fake.SplitCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) SplitCompositeKeyArgsForCall(i int) string { + fake.splitCompositeKeyMutex.RLock() + defer fake.splitCompositeKeyMutex.RUnlock() + argsForCall := fake.splitCompositeKeyArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) SplitCompositeKeyReturns(result1 string, result2 []string, result3 error) { + fake.splitCompositeKeyMutex.Lock() + defer fake.splitCompositeKeyMutex.Unlock() + fake.SplitCompositeKeyStub = nil + fake.splitCompositeKeyReturns = struct { + result1 string + result2 []string + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) SplitCompositeKeyReturnsOnCall(i int, result1 string, result2 []string, result3 error) { + fake.splitCompositeKeyMutex.Lock() + defer fake.splitCompositeKeyMutex.Unlock() + fake.SplitCompositeKeyStub = nil + if fake.splitCompositeKeyReturnsOnCall == nil { + fake.splitCompositeKeyReturnsOnCall = make(map[int]struct { + result1 string + result2 []string + result3 error + }) + } + fake.splitCompositeKeyReturnsOnCall[i] = struct { + result1 string + result2 []string + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createCompositeKeyMutex.RLock() + defer fake.createCompositeKeyMutex.RUnlock() + fake.delPrivateDataMutex.RLock() + defer fake.delPrivateDataMutex.RUnlock() + fake.delStateMutex.RLock() + defer fake.delStateMutex.RUnlock() + fake.getArgsMutex.RLock() + defer fake.getArgsMutex.RUnlock() + fake.getArgsSliceMutex.RLock() + defer fake.getArgsSliceMutex.RUnlock() + fake.getBindingMutex.RLock() + defer fake.getBindingMutex.RUnlock() + fake.getChannelIDMutex.RLock() + defer fake.getChannelIDMutex.RUnlock() + fake.getCreatorMutex.RLock() + defer fake.getCreatorMutex.RUnlock() + fake.getDecorationsMutex.RLock() + defer fake.getDecorationsMutex.RUnlock() + fake.getFunctionAndParametersMutex.RLock() + defer fake.getFunctionAndParametersMutex.RUnlock() + fake.getHistoryForKeyMutex.RLock() + defer fake.getHistoryForKeyMutex.RUnlock() + fake.getPrivateDataMutex.RLock() + defer fake.getPrivateDataMutex.RUnlock() + fake.getPrivateDataByPartialCompositeKeyMutex.RLock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.RUnlock() + fake.getPrivateDataByRangeMutex.RLock() + defer fake.getPrivateDataByRangeMutex.RUnlock() + fake.getPrivateDataHashMutex.RLock() + defer fake.getPrivateDataHashMutex.RUnlock() + fake.getPrivateDataQueryResultMutex.RLock() + defer fake.getPrivateDataQueryResultMutex.RUnlock() + fake.getPrivateDataValidationParameterMutex.RLock() + defer fake.getPrivateDataValidationParameterMutex.RUnlock() + fake.getQueryResultMutex.RLock() + defer fake.getQueryResultMutex.RUnlock() + fake.getQueryResultWithPaginationMutex.RLock() + defer fake.getQueryResultWithPaginationMutex.RUnlock() + fake.getSignedProposalMutex.RLock() + defer fake.getSignedProposalMutex.RUnlock() + fake.getStateMutex.RLock() + defer fake.getStateMutex.RUnlock() + fake.getStateByPartialCompositeKeyMutex.RLock() + defer fake.getStateByPartialCompositeKeyMutex.RUnlock() + fake.getStateByPartialCompositeKeyWithPaginationMutex.RLock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.RUnlock() + fake.getStateByRangeMutex.RLock() + defer fake.getStateByRangeMutex.RUnlock() + fake.getStateByRangeWithPaginationMutex.RLock() + defer fake.getStateByRangeWithPaginationMutex.RUnlock() + fake.getStateValidationParameterMutex.RLock() + defer fake.getStateValidationParameterMutex.RUnlock() + fake.getStringArgsMutex.RLock() + defer fake.getStringArgsMutex.RUnlock() + fake.getTransientMutex.RLock() + defer fake.getTransientMutex.RUnlock() + fake.getTxIDMutex.RLock() + defer fake.getTxIDMutex.RUnlock() + fake.getTxTimestampMutex.RLock() + defer fake.getTxTimestampMutex.RUnlock() + fake.invokeChaincodeMutex.RLock() + defer fake.invokeChaincodeMutex.RUnlock() + fake.putPrivateDataMutex.RLock() + defer fake.putPrivateDataMutex.RUnlock() + fake.putStateMutex.RLock() + defer fake.putStateMutex.RUnlock() + fake.setEventMutex.RLock() + defer fake.setEventMutex.RUnlock() + fake.setPrivateDataValidationParameterMutex.RLock() + defer fake.setPrivateDataValidationParameterMutex.RUnlock() + fake.setStateValidationParameterMutex.RLock() + defer fake.setStateValidationParameterMutex.RUnlock() + fake.splitCompositeKeyMutex.RLock() + defer fake.splitCompositeKeyMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *ChaincodeStub) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/asset-transfer-basic/chaincode-go/chaincode/mocks/statequeryiterator.go b/asset-transfer-basic/chaincode-go/chaincode/mocks/statequeryiterator.go new file mode 100644 index 0000000..27e3034 --- /dev/null +++ b/asset-transfer-basic/chaincode-go/chaincode/mocks/statequeryiterator.go @@ -0,0 +1,232 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/hyperledger/fabric-protos-go/ledger/queryresult" +) + +type StateQueryIterator struct { + CloseStub func() error + closeMutex sync.RWMutex + closeArgsForCall []struct { + } + closeReturns struct { + result1 error + } + closeReturnsOnCall map[int]struct { + result1 error + } + HasNextStub func() bool + hasNextMutex sync.RWMutex + hasNextArgsForCall []struct { + } + hasNextReturns struct { + result1 bool + } + hasNextReturnsOnCall map[int]struct { + result1 bool + } + NextStub func() (*queryresult.KV, error) + nextMutex sync.RWMutex + nextArgsForCall []struct { + } + nextReturns struct { + result1 *queryresult.KV + result2 error + } + nextReturnsOnCall map[int]struct { + result1 *queryresult.KV + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *StateQueryIterator) Close() error { + fake.closeMutex.Lock() + ret, specificReturn := fake.closeReturnsOnCall[len(fake.closeArgsForCall)] + fake.closeArgsForCall = append(fake.closeArgsForCall, struct { + }{}) + fake.recordInvocation("Close", []interface{}{}) + fake.closeMutex.Unlock() + if fake.CloseStub != nil { + return fake.CloseStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.closeReturns + return fakeReturns.result1 +} + +func (fake *StateQueryIterator) CloseCallCount() int { + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + return len(fake.closeArgsForCall) +} + +func (fake *StateQueryIterator) CloseCalls(stub func() error) { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.CloseStub = stub +} + +func (fake *StateQueryIterator) CloseReturns(result1 error) { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.CloseStub = nil + fake.closeReturns = struct { + result1 error + }{result1} +} + +func (fake *StateQueryIterator) CloseReturnsOnCall(i int, result1 error) { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.CloseStub = nil + if fake.closeReturnsOnCall == nil { + fake.closeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.closeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *StateQueryIterator) HasNext() bool { + fake.hasNextMutex.Lock() + ret, specificReturn := fake.hasNextReturnsOnCall[len(fake.hasNextArgsForCall)] + fake.hasNextArgsForCall = append(fake.hasNextArgsForCall, struct { + }{}) + fake.recordInvocation("HasNext", []interface{}{}) + fake.hasNextMutex.Unlock() + if fake.HasNextStub != nil { + return fake.HasNextStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.hasNextReturns + return fakeReturns.result1 +} + +func (fake *StateQueryIterator) HasNextCallCount() int { + fake.hasNextMutex.RLock() + defer fake.hasNextMutex.RUnlock() + return len(fake.hasNextArgsForCall) +} + +func (fake *StateQueryIterator) HasNextCalls(stub func() bool) { + fake.hasNextMutex.Lock() + defer fake.hasNextMutex.Unlock() + fake.HasNextStub = stub +} + +func (fake *StateQueryIterator) HasNextReturns(result1 bool) { + fake.hasNextMutex.Lock() + defer fake.hasNextMutex.Unlock() + fake.HasNextStub = nil + fake.hasNextReturns = struct { + result1 bool + }{result1} +} + +func (fake *StateQueryIterator) HasNextReturnsOnCall(i int, result1 bool) { + fake.hasNextMutex.Lock() + defer fake.hasNextMutex.Unlock() + fake.HasNextStub = nil + if fake.hasNextReturnsOnCall == nil { + fake.hasNextReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.hasNextReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *StateQueryIterator) Next() (*queryresult.KV, error) { + fake.nextMutex.Lock() + ret, specificReturn := fake.nextReturnsOnCall[len(fake.nextArgsForCall)] + fake.nextArgsForCall = append(fake.nextArgsForCall, struct { + }{}) + fake.recordInvocation("Next", []interface{}{}) + fake.nextMutex.Unlock() + if fake.NextStub != nil { + return fake.NextStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.nextReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *StateQueryIterator) NextCallCount() int { + fake.nextMutex.RLock() + defer fake.nextMutex.RUnlock() + return len(fake.nextArgsForCall) +} + +func (fake *StateQueryIterator) NextCalls(stub func() (*queryresult.KV, error)) { + fake.nextMutex.Lock() + defer fake.nextMutex.Unlock() + fake.NextStub = stub +} + +func (fake *StateQueryIterator) NextReturns(result1 *queryresult.KV, result2 error) { + fake.nextMutex.Lock() + defer fake.nextMutex.Unlock() + fake.NextStub = nil + fake.nextReturns = struct { + result1 *queryresult.KV + result2 error + }{result1, result2} +} + +func (fake *StateQueryIterator) NextReturnsOnCall(i int, result1 *queryresult.KV, result2 error) { + fake.nextMutex.Lock() + defer fake.nextMutex.Unlock() + fake.NextStub = nil + if fake.nextReturnsOnCall == nil { + fake.nextReturnsOnCall = make(map[int]struct { + result1 *queryresult.KV + result2 error + }) + } + fake.nextReturnsOnCall[i] = struct { + result1 *queryresult.KV + result2 error + }{result1, result2} +} + +func (fake *StateQueryIterator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + fake.hasNextMutex.RLock() + defer fake.hasNextMutex.RUnlock() + fake.nextMutex.RLock() + defer fake.nextMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *StateQueryIterator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/asset-transfer-basic/chaincode-go/chaincode/mocks/transaction.go b/asset-transfer-basic/chaincode-go/chaincode/mocks/transaction.go new file mode 100644 index 0000000..eea37db --- /dev/null +++ b/asset-transfer-basic/chaincode-go/chaincode/mocks/transaction.go @@ -0,0 +1,164 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/hyperledger/fabric-chaincode-go/pkg/cid" + "github.com/hyperledger/fabric-chaincode-go/shim" +) + +type TransactionContext struct { + GetClientIdentityStub func() cid.ClientIdentity + getClientIdentityMutex sync.RWMutex + getClientIdentityArgsForCall []struct { + } + getClientIdentityReturns struct { + result1 cid.ClientIdentity + } + getClientIdentityReturnsOnCall map[int]struct { + result1 cid.ClientIdentity + } + GetStubStub func() shim.ChaincodeStubInterface + getStubMutex sync.RWMutex + getStubArgsForCall []struct { + } + getStubReturns struct { + result1 shim.ChaincodeStubInterface + } + getStubReturnsOnCall map[int]struct { + result1 shim.ChaincodeStubInterface + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *TransactionContext) GetClientIdentity() cid.ClientIdentity { + fake.getClientIdentityMutex.Lock() + ret, specificReturn := fake.getClientIdentityReturnsOnCall[len(fake.getClientIdentityArgsForCall)] + fake.getClientIdentityArgsForCall = append(fake.getClientIdentityArgsForCall, struct { + }{}) + fake.recordInvocation("GetClientIdentity", []interface{}{}) + fake.getClientIdentityMutex.Unlock() + if fake.GetClientIdentityStub != nil { + return fake.GetClientIdentityStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getClientIdentityReturns + return fakeReturns.result1 +} + +func (fake *TransactionContext) GetClientIdentityCallCount() int { + fake.getClientIdentityMutex.RLock() + defer fake.getClientIdentityMutex.RUnlock() + return len(fake.getClientIdentityArgsForCall) +} + +func (fake *TransactionContext) GetClientIdentityCalls(stub func() cid.ClientIdentity) { + fake.getClientIdentityMutex.Lock() + defer fake.getClientIdentityMutex.Unlock() + fake.GetClientIdentityStub = stub +} + +func (fake *TransactionContext) GetClientIdentityReturns(result1 cid.ClientIdentity) { + fake.getClientIdentityMutex.Lock() + defer fake.getClientIdentityMutex.Unlock() + fake.GetClientIdentityStub = nil + fake.getClientIdentityReturns = struct { + result1 cid.ClientIdentity + }{result1} +} + +func (fake *TransactionContext) GetClientIdentityReturnsOnCall(i int, result1 cid.ClientIdentity) { + fake.getClientIdentityMutex.Lock() + defer fake.getClientIdentityMutex.Unlock() + fake.GetClientIdentityStub = nil + if fake.getClientIdentityReturnsOnCall == nil { + fake.getClientIdentityReturnsOnCall = make(map[int]struct { + result1 cid.ClientIdentity + }) + } + fake.getClientIdentityReturnsOnCall[i] = struct { + result1 cid.ClientIdentity + }{result1} +} + +func (fake *TransactionContext) GetStub() shim.ChaincodeStubInterface { + fake.getStubMutex.Lock() + ret, specificReturn := fake.getStubReturnsOnCall[len(fake.getStubArgsForCall)] + fake.getStubArgsForCall = append(fake.getStubArgsForCall, struct { + }{}) + fake.recordInvocation("GetStub", []interface{}{}) + fake.getStubMutex.Unlock() + if fake.GetStubStub != nil { + return fake.GetStubStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getStubReturns + return fakeReturns.result1 +} + +func (fake *TransactionContext) GetStubCallCount() int { + fake.getStubMutex.RLock() + defer fake.getStubMutex.RUnlock() + return len(fake.getStubArgsForCall) +} + +func (fake *TransactionContext) GetStubCalls(stub func() shim.ChaincodeStubInterface) { + fake.getStubMutex.Lock() + defer fake.getStubMutex.Unlock() + fake.GetStubStub = stub +} + +func (fake *TransactionContext) GetStubReturns(result1 shim.ChaincodeStubInterface) { + fake.getStubMutex.Lock() + defer fake.getStubMutex.Unlock() + fake.GetStubStub = nil + fake.getStubReturns = struct { + result1 shim.ChaincodeStubInterface + }{result1} +} + +func (fake *TransactionContext) GetStubReturnsOnCall(i int, result1 shim.ChaincodeStubInterface) { + fake.getStubMutex.Lock() + defer fake.getStubMutex.Unlock() + fake.GetStubStub = nil + if fake.getStubReturnsOnCall == nil { + fake.getStubReturnsOnCall = make(map[int]struct { + result1 shim.ChaincodeStubInterface + }) + } + fake.getStubReturnsOnCall[i] = struct { + result1 shim.ChaincodeStubInterface + }{result1} +} + +func (fake *TransactionContext) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getClientIdentityMutex.RLock() + defer fake.getClientIdentityMutex.RUnlock() + fake.getStubMutex.RLock() + defer fake.getStubMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *TransactionContext) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/asset-transfer-basic/chaincode-go/chaincode/smartcontract.go b/asset-transfer-basic/chaincode-go/chaincode/smartcontract.go new file mode 100644 index 0000000..71e8dd8 --- /dev/null +++ b/asset-transfer-basic/chaincode-go/chaincode/smartcontract.go @@ -0,0 +1,185 @@ +package chaincode + +import ( + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// SmartContract provides functions for managing an Asset +type SmartContract struct { + contractapi.Contract +} + +// Asset describes basic details of what makes up a simple asset +type Asset struct { + ID string `json:"ID"` + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` + AppraisedValue int `json:"appraisedValue"` +} + +// InitLedger adds a base set of assets to the ledger +func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { + assets := []Asset{ + {ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300}, + {ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400}, + {ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500}, + {ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600}, + {ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700}, + {ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800}, + } + + for _, asset := range assets { + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(asset.ID, assetJSON) + if err != nil { + return fmt.Errorf("failed to put to world state. %v", err) + } + } + + return nil +} + +// CreateAsset issues a new asset to the world state with given details. +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if exists { + return fmt.Errorf("the asset %s already exists", id) + } + + asset := Asset{ + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + } + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// ReadAsset returns the asset stored in the world state with given id. +func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) { + assetJSON, err := ctx.GetStub().GetState(id) + if err != nil { + return nil, fmt.Errorf("failed to read from world state: %v", err) + } + if assetJSON == nil { + return nil, fmt.Errorf("the asset %s does not exist", id) + } + + var asset Asset + err = json.Unmarshal(assetJSON, &asset) + if err != nil { + return nil, err + } + + return &asset, nil +} + +// UpdateAsset updates an existing asset in the world state with provided parameters. +func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("the asset %s does not exist", id) + } + + // overwriting original asset with new asset + asset := Asset{ + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + } + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// DeleteAsset deletes an given asset from the world state. +func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error { + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("the asset %s does not exist", id) + } + + return ctx.GetStub().DelState(id) +} + +// AssetExists returns true when asset with given ID exists in world state +func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) { + assetJSON, err := ctx.GetStub().GetState(id) + if err != nil { + return false, fmt.Errorf("failed to read from world state: %v", err) + } + + return assetJSON != nil, nil +} + +// TransferAsset updates the owner field of asset with given id in world state. +func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error { + asset, err := s.ReadAsset(ctx, id) + if err != nil { + return err + } + + asset.Owner = newOwner + assetJSON, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(id, assetJSON) +} + +// GetAllAssets returns all assets found in world state +func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) { + // range query with empty string for startKey and endKey does an + // open-ended query of all assets in the chaincode namespace. + resultsIterator, err := ctx.GetStub().GetStateByRange("", "") + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + var assets []*Asset + for resultsIterator.HasNext() { + queryResponse, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + var asset Asset + err = json.Unmarshal(queryResponse.Value, &asset) + if err != nil { + return nil, err + } + assets = append(assets, &asset) + } + + return assets, nil +} diff --git a/asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go b/asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go new file mode 100644 index 0000000..cb001de --- /dev/null +++ b/asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go @@ -0,0 +1,184 @@ +package chaincode_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/hyperledger/fabric-protos-go/ledger/queryresult" + "github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode" + "github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode/mocks" + "github.com/stretchr/testify/require" +) + +//go:generate counterfeiter -o mocks/transaction.go -fake-name TransactionContext . transactionContext +type transactionContext interface { + contractapi.TransactionContextInterface +} + +//go:generate counterfeiter -o mocks/chaincodestub.go -fake-name ChaincodeStub . chaincodeStub +type chaincodeStub interface { + shim.ChaincodeStubInterface +} + +//go:generate counterfeiter -o mocks/statequeryiterator.go -fake-name StateQueryIterator . stateQueryIterator +type stateQueryIterator interface { + shim.StateQueryIteratorInterface +} + +func TestInitLedger(t *testing.T) { + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + assetTransfer := chaincode.SmartContract{} + err := assetTransfer.InitLedger(transactionContext) + require.NoError(t, err) + + chaincodeStub.PutStateReturns(fmt.Errorf("failed inserting key")) + err = assetTransfer.InitLedger(transactionContext) + require.EqualError(t, err, "failed to put to world state. failed inserting key") +} + +func TestCreateAsset(t *testing.T) { + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + assetTransfer := chaincode.SmartContract{} + err := assetTransfer.CreateAsset(transactionContext, "", "", 0, "", 0) + require.NoError(t, err) + + chaincodeStub.GetStateReturns([]byte{}, nil) + err = assetTransfer.CreateAsset(transactionContext, "asset1", "", 0, "", 0) + require.EqualError(t, err, "the asset asset1 already exists") + + chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset")) + err = assetTransfer.CreateAsset(transactionContext, "asset1", "", 0, "", 0) + require.EqualError(t, err, "failed to read from world state: unable to retrieve asset") +} + +func TestReadAsset(t *testing.T) { + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + expectedAsset := &chaincode.Asset{ID: "asset1"} + bytes, err := json.Marshal(expectedAsset) + require.NoError(t, err) + + chaincodeStub.GetStateReturns(bytes, nil) + assetTransfer := chaincode.SmartContract{} + asset, err := assetTransfer.ReadAsset(transactionContext, "") + require.NoError(t, err) + require.Equal(t, expectedAsset, asset) + + chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset")) + _, err = assetTransfer.ReadAsset(transactionContext, "") + require.EqualError(t, err, "failed to read from world state: unable to retrieve asset") + + chaincodeStub.GetStateReturns(nil, nil) + asset, err = assetTransfer.ReadAsset(transactionContext, "asset1") + require.EqualError(t, err, "the asset asset1 does not exist") + require.Nil(t, asset) +} + +func TestUpdateAsset(t *testing.T) { + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + expectedAsset := &chaincode.Asset{ID: "asset1"} + bytes, err := json.Marshal(expectedAsset) + require.NoError(t, err) + + chaincodeStub.GetStateReturns(bytes, nil) + assetTransfer := chaincode.SmartContract{} + err = assetTransfer.UpdateAsset(transactionContext, "", "", 0, "", 0) + require.NoError(t, err) + + chaincodeStub.GetStateReturns(nil, nil) + err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", 0, "", 0) + require.EqualError(t, err, "the asset asset1 does not exist") + + chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset")) + err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", 0, "", 0) + require.EqualError(t, err, "failed to read from world state: unable to retrieve asset") +} + +func TestDeleteAsset(t *testing.T) { + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + asset := &chaincode.Asset{ID: "asset1"} + bytes, err := json.Marshal(asset) + require.NoError(t, err) + + chaincodeStub.GetStateReturns(bytes, nil) + chaincodeStub.DelStateReturns(nil) + assetTransfer := chaincode.SmartContract{} + err = assetTransfer.DeleteAsset(transactionContext, "") + require.NoError(t, err) + + chaincodeStub.GetStateReturns(nil, nil) + err = assetTransfer.DeleteAsset(transactionContext, "asset1") + require.EqualError(t, err, "the asset asset1 does not exist") + + chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset")) + err = assetTransfer.DeleteAsset(transactionContext, "") + require.EqualError(t, err, "failed to read from world state: unable to retrieve asset") +} + +func TestTransferAsset(t *testing.T) { + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + asset := &chaincode.Asset{ID: "asset1"} + bytes, err := json.Marshal(asset) + require.NoError(t, err) + + chaincodeStub.GetStateReturns(bytes, nil) + assetTransfer := chaincode.SmartContract{} + err = assetTransfer.TransferAsset(transactionContext, "", "") + require.NoError(t, err) + + chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset")) + err = assetTransfer.TransferAsset(transactionContext, "", "") + require.EqualError(t, err, "failed to read from world state: unable to retrieve asset") +} + +func TestGetAllAssets(t *testing.T) { + asset := &chaincode.Asset{ID: "asset1"} + bytes, err := json.Marshal(asset) + require.NoError(t, err) + + iterator := &mocks.StateQueryIterator{} + iterator.HasNextReturnsOnCall(0, true) + iterator.HasNextReturnsOnCall(1, false) + iterator.NextReturns(&queryresult.KV{Value: bytes}, nil) + + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + chaincodeStub.GetStateByRangeReturns(iterator, nil) + assetTransfer := &chaincode.SmartContract{} + assets, err := assetTransfer.GetAllAssets(transactionContext) + require.NoError(t, err) + require.Equal(t, []*chaincode.Asset{asset}, assets) + + iterator.HasNextReturns(true) + iterator.NextReturns(nil, fmt.Errorf("failed retrieving next item")) + assets, err = assetTransfer.GetAllAssets(transactionContext) + require.EqualError(t, err, "failed retrieving next item") + require.Nil(t, assets) + + chaincodeStub.GetStateByRangeReturns(nil, fmt.Errorf("failed retrieving all assets")) + assets, err = assetTransfer.GetAllAssets(transactionContext) + require.EqualError(t, err, "failed retrieving all assets") + require.Nil(t, assets) +} diff --git a/asset-transfer-basic/chaincode-go/go.mod b/asset-transfer-basic/chaincode-go/go.mod new file mode 100644 index 0000000..a39adb0 --- /dev/null +++ b/asset-transfer-basic/chaincode-go/go.mod @@ -0,0 +1,11 @@ +module github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go + +go 1.14 + +require ( + github.com/golang/protobuf v1.3.2 + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212 + github.com/hyperledger/fabric-contract-api-go v1.1.0 + github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e + github.com/stretchr/testify v1.5.1 +) diff --git a/asset-transfer-basic/chaincode-go/go.sum b/asset-transfer-basic/chaincode-go/go.sum new file mode 100644 index 0000000..a159a45 --- /dev/null +++ b/asset-transfer-basic/chaincode-go/go.sum @@ -0,0 +1,146 @@ +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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/asset-transfer-basic/chaincode-java/.gitattributes b/asset-transfer-basic/chaincode-java/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/asset-transfer-basic/chaincode-java/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/asset-transfer-basic/chaincode-java/build.gradle b/asset-transfer-basic/chaincode-java/build.gradle new file mode 100644 index 0000000..5f90c5a --- /dev/null +++ b/asset-transfer-basic/chaincode-java/build.gradle @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'application' + id 'checkstyle' + id 'jacoco' +} + +group 'org.hyperledger.fabric.samples' +version '1.0-SNAPSHOT' + +dependencies { + + compileOnly 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + implementation 'com.owlike:genson:1.5' + testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +repositories { + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + jcenter() + maven { + url 'https://jitpack.io' + } +} + +application { + mainClass = 'org.hyperledger.fabric.contract.ContractRouter' +} + +checkstyle { + toolVersion '8.21' + configFile file("config/checkstyle/checkstyle.xml") +} + +checkstyleMain { + source ='src/main/java' +} + +checkstyleTest { + source ='src/test/java' +} + +jacocoTestReport { + dependsOn test +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.9 + } + } + } + + finalizedBy jacocoTestReport +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +check.dependsOn jacocoTestCoverageVerification +installDist.dependsOn check diff --git a/asset-transfer-basic/chaincode-java/config/checkstyle/checkstyle.xml b/asset-transfer-basic/chaincode-java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..acd5df4 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/config/checkstyle/checkstyle.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/asset-transfer-basic/chaincode-java/config/checkstyle/suppressions.xml b/asset-transfer-basic/chaincode-java/config/checkstyle/suppressions.xml new file mode 100644 index 0000000..8c44b0a --- /dev/null +++ b/asset-transfer-basic/chaincode-java/config/checkstyle/suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/asset-transfer-basic/chaincode-java/gradlew.bat b/asset-transfer-basic/chaincode-java/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/asset-transfer-basic/chaincode-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/asset-transfer-basic/chaincode-java/settings.gradle b/asset-transfer-basic/chaincode-java/settings.gradle new file mode 100644 index 0000000..2633c4b --- /dev/null +++ b/asset-transfer-basic/chaincode-java/settings.gradle @@ -0,0 +1,5 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'basic' diff --git a/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/Asset.java b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/Asset.java new file mode 100644 index 0000000..803f22f --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/Asset.java @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import java.util.Objects; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +import com.owlike.genson.annotation.JsonProperty; + +@DataType() +public final class Asset { + + @Property() + private final String assetID; + + @Property() + private final String color; + + @Property() + private final int size; + + @Property() + private final String owner; + + @Property() + private final int appraisedValue; + + public String getAssetID() { + return assetID; + } + + public String getColor() { + return color; + } + + public int getSize() { + return size; + } + + public String getOwner() { + return owner; + } + + public int getAppraisedValue() { + return appraisedValue; + } + + public Asset(@JsonProperty("assetID") final String assetID, @JsonProperty("color") final String color, + @JsonProperty("size") final int size, @JsonProperty("owner") final String owner, + @JsonProperty("appraisedValue") final int appraisedValue) { + this.assetID = assetID; + this.color = color; + this.size = size; + this.owner = owner; + this.appraisedValue = appraisedValue; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + Asset other = (Asset) obj; + + return Objects.deepEquals( + new String[] {getAssetID(), getColor(), getOwner()}, + new String[] {other.getAssetID(), other.getColor(), other.getOwner()}) + && + Objects.deepEquals( + new int[] {getSize(), getAppraisedValue()}, + new int[] {other.getSize(), other.getAppraisedValue()}); + } + + @Override + public int hashCode() { + return Objects.hash(getAssetID(), getColor(), getSize(), getOwner(), getAppraisedValue()); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + " [assetID=" + assetID + ", color=" + + color + ", size=" + size + ", owner=" + owner + ", appraisedValue=" + appraisedValue + "]"; + } +} diff --git a/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/AssetTransfer.java b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/AssetTransfer.java new file mode 100644 index 0000000..465d469 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/AssetTransfer.java @@ -0,0 +1,236 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import java.util.ArrayList; +import java.util.List; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contact; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; + +import com.owlike.genson.Genson; + +@Contract( + name = "basic", + info = @Info( + title = "Asset Transfer", + description = "The hyperlegendary asset transfer", + version = "0.0.1-SNAPSHOT", + license = @License( + name = "Apache 2.0 License", + url = "http://www.apache.org/licenses/LICENSE-2.0.html"), + contact = @Contact( + email = "a.transfer@example.com", + name = "Adrian Transfer", + url = "https://hyperledger.example.com"))) +@Default +public final class AssetTransfer implements ContractInterface { + + private final Genson genson = new Genson(); + + private enum AssetTransferErrors { + ASSET_NOT_FOUND, + ASSET_ALREADY_EXISTS + } + + /** + * Creates some initial assets on the ledger. + * + * @param ctx the transaction context + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void InitLedger(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + + CreateAsset(ctx, "asset1", "blue", 5, "Tomoko", 300); + CreateAsset(ctx, "asset2", "red", 5, "Brad", 400); + CreateAsset(ctx, "asset3", "green", 10, "Jin Soo", 500); + CreateAsset(ctx, "asset4", "yellow", 10, "Max", 600); + CreateAsset(ctx, "asset5", "black", 15, "Adrian", 700); + CreateAsset(ctx, "asset6", "white", 15, "Michel", 700); + + } + + /** + * Creates a new asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the new asset + * @param color the color of the new asset + * @param size the size for the new asset + * @param owner the owner of the new asset + * @param appraisedValue the appraisedValue of the new asset + * @return the created asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset CreateAsset(final Context ctx, final String assetID, final String color, final int size, + final String owner, final int appraisedValue) { + ChaincodeStub stub = ctx.getStub(); + + if (AssetExists(ctx, assetID)) { + String errorMessage = String.format("Asset %s already exists", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString()); + } + + Asset asset = new Asset(assetID, color, size, owner, appraisedValue); + String assetJSON = genson.serialize(asset); + stub.putStringState(assetID, assetJSON); + + return asset; + } + + /** + * Retrieves an asset with the specified ID from the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset + * @return the asset found on the ledger if there was one + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public Asset ReadAsset(final Context ctx, final String assetID) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetID); + + if (assetJSON == null || assetJSON.isEmpty()) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + Asset asset = genson.deserialize(assetJSON, Asset.class); + return asset; + } + + /** + * Updates the properties of an asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset being updated + * @param color the color of the asset being updated + * @param size the size of the asset being updated + * @param owner the owner of the asset being updated + * @param appraisedValue the appraisedValue of the asset being updated + * @return the transferred asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset UpdateAsset(final Context ctx, final String assetID, final String color, final int size, + final String owner, final int appraisedValue) { + ChaincodeStub stub = ctx.getStub(); + + if (!AssetExists(ctx, assetID)) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + Asset newAsset = new Asset(assetID, color, size, owner, appraisedValue); + String newAssetJSON = genson.serialize(newAsset); + stub.putStringState(assetID, newAssetJSON); + + return newAsset; + } + + /** + * Deletes asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset being deleted + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void DeleteAsset(final Context ctx, final String assetID) { + ChaincodeStub stub = ctx.getStub(); + + if (!AssetExists(ctx, assetID)) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + stub.delState(assetID); + } + + /** + * Checks the existence of the asset on the ledger + * + * @param ctx the transaction context + * @param assetID the ID of the asset + * @return boolean indicating the existence of the asset + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public boolean AssetExists(final Context ctx, final String assetID) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetID); + + return (assetJSON != null && !assetJSON.isEmpty()); + } + + /** + * Changes the owner of a asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset being transferred + * @param newOwner the new owner + * @return the updated asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset TransferAsset(final Context ctx, final String assetID, final String newOwner) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetID); + + if (assetJSON == null || assetJSON.isEmpty()) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + Asset asset = genson.deserialize(assetJSON, Asset.class); + + Asset newAsset = new Asset(asset.getAssetID(), asset.getColor(), asset.getSize(), newOwner, asset.getAppraisedValue()); + String newAssetJSON = genson.serialize(newAsset); + stub.putStringState(assetID, newAssetJSON); + + return newAsset; + } + + /** + * Retrieves all assets from the ledger. + * + * @param ctx the transaction context + * @return array of assets found on the ledger + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String GetAllAssets(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + + List queryResults = new ArrayList(); + + // To retrieve all assets from the ledger use getStateByRange with empty startKey & endKey. + // Giving empty startKey & endKey is interpreted as all the keys from beginning to end. + // As another example, if you use startKey = 'asset0', endKey = 'asset9' , + // then getStateByRange will retrieve asset with keys between asset0 (inclusive) and asset9 (exclusive) in lexical order. + QueryResultsIterator results = stub.getStateByRange("", ""); + + for (KeyValue result: results) { + Asset asset = genson.deserialize(result.getStringValue(), Asset.class); + queryResults.add(asset); + System.out.println(asset.toString()); + } + + final String response = genson.serialize(queryResults); + + return response; + } +} diff --git a/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTest.java b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTest.java new file mode 100644 index 0000000..7da94ca --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTest.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public final class AssetTest { + + @Nested + class Equality { + + @Test + public void isReflexive() { + Asset asset = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(asset).isEqualTo(asset); + } + + @Test + public void isSymmetric() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetB = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(assetA).isEqualTo(assetB); + assertThat(assetB).isEqualTo(assetA); + } + + @Test + public void isTransitive() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetB = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetC = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(assetA).isEqualTo(assetB); + assertThat(assetB).isEqualTo(assetC); + assertThat(assetA).isEqualTo(assetC); + } + + @Test + public void handlesInequality() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetB = new Asset("asset2", "Red", 40, "Lady", 200); + + assertThat(assetA).isNotEqualTo(assetB); + } + + @Test + public void handlesOtherObjects() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + String assetB = "not a asset"; + + assertThat(assetA).isNotEqualTo(assetB); + } + + @Test + public void handlesNull() { + Asset asset = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(asset).isNotEqualTo(null); + } + } + + @Test + public void toStringIdentifiesAsset() { + Asset asset = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(asset.toString()).isEqualTo("Asset@e04f6c53 [assetID=asset1, color=Blue, size=20, owner=Guy, appraisedValue=100]"); + } +} diff --git a/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTransferTest.java b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTransferTest.java new file mode 100644 index 0000000..cbbb172 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTransferTest.java @@ -0,0 +1,305 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +public final class AssetTransferTest { + + private final class MockKeyValue implements KeyValue { + + private final String key; + private final String value; + + MockKeyValue(final String key, final String value) { + super(); + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public String getStringValue() { + return this.value; + } + + @Override + public byte[] getValue() { + return this.value.getBytes(); + } + + } + + private final class MockAssetResultsIterator implements QueryResultsIterator { + + private final List assetList; + + MockAssetResultsIterator() { + super(); + + assetList = new ArrayList(); + + assetList.add(new MockKeyValue("asset1", + "{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }")); + assetList.add(new MockKeyValue("asset2", + "{ \"assetID\": \"asset2\", \"color\": \"red\", \"size\": 5,\"owner\": \"Brad\", \"appraisedValue\": 400 }")); + assetList.add(new MockKeyValue("asset3", + "{ \"assetID\": \"asset3\", \"color\": \"green\", \"size\": 10,\"owner\": \"Jin Soo\", \"appraisedValue\": 500 }")); + assetList.add(new MockKeyValue("asset4", + "{ \"assetID\": \"asset4\", \"color\": \"yellow\", \"size\": 10,\"owner\": \"Max\", \"appraisedValue\": 600 }")); + assetList.add(new MockKeyValue("asset5", + "{ \"assetID\": \"asset5\", \"color\": \"black\", \"size\": 15,\"owner\": \"Adrian\", \"appraisedValue\": 700 }")); + assetList.add(new MockKeyValue("asset6", + "{ \"assetID\": \"asset6\", \"color\": \"white\", \"size\": 15,\"owner\": \"Michel\", \"appraisedValue\": 800 }")); + } + + @Override + public Iterator iterator() { + return assetList.iterator(); + } + + @Override + public void close() throws Exception { + // do nothing + } + + } + + @Test + public void invokeUnknownTransaction() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + + Throwable thrown = catchThrowable(() -> { + contract.unknownTransaction(ctx); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Undefined contract method called"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo(null); + + verifyZeroInteractions(ctx); + } + + @Nested + class InvokeReadAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }"); + + Asset asset = contract.ReadAsset(ctx, "asset1"); + + assertThat(asset).isEqualTo(new Asset("asset1", "blue", 5, "Tomoko", 300)); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.ReadAsset(ctx, "asset1"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } + + @Test + void invokeInitLedgerTransaction() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + + contract.InitLedger(ctx); + + InOrder inOrder = inOrder(stub); + inOrder.verify(stub).putStringState("asset1", "{\"appraisedValue\":300,\"assetID\":\"asset1\",\"color\":\"blue\",\"owner\":\"Tomoko\",\"size\":5}"); + inOrder.verify(stub).putStringState("asset2", "{\"appraisedValue\":400,\"assetID\":\"asset2\",\"color\":\"red\",\"owner\":\"Brad\",\"size\":5}"); + inOrder.verify(stub).putStringState("asset3", "{\"appraisedValue\":500,\"assetID\":\"asset3\",\"color\":\"green\",\"owner\":\"Jin Soo\",\"size\":10}"); + inOrder.verify(stub).putStringState("asset4", "{\"appraisedValue\":600,\"assetID\":\"asset4\",\"color\":\"yellow\",\"owner\":\"Max\",\"size\":10}"); + inOrder.verify(stub).putStringState("asset5", "{\"appraisedValue\":700,\"assetID\":\"asset5\",\"color\":\"black\",\"owner\":\"Adrian\",\"size\":15}"); + + } + + @Nested + class InvokeCreateAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }"); + + Throwable thrown = catchThrowable(() -> { + contract.CreateAsset(ctx, "asset1", "blue", 45, "Siobhán", 60); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 already exists"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_ALREADY_EXISTS".getBytes()); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Asset asset = contract.CreateAsset(ctx, "asset1", "blue", 45, "Siobhán", 60); + + assertThat(asset).isEqualTo(new Asset("asset1", "blue", 45, "Siobhán", 60)); + } + } + + @Test + void invokeGetAllAssetsTransaction() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStateByRange("", "")).thenReturn(new MockAssetResultsIterator()); + + String assets = contract.GetAllAssets(ctx); + + assertThat(assets).isEqualTo("[{\"appraisedValue\":300,\"assetID\":\"asset1\",\"color\":\"blue\",\"owner\":\"Tomoko\",\"size\":5}," + + "{\"appraisedValue\":400,\"assetID\":\"asset2\",\"color\":\"red\",\"owner\":\"Brad\",\"size\":5}," + + "{\"appraisedValue\":500,\"assetID\":\"asset3\",\"color\":\"green\",\"owner\":\"Jin Soo\",\"size\":10}," + + "{\"appraisedValue\":600,\"assetID\":\"asset4\",\"color\":\"yellow\",\"owner\":\"Max\",\"size\":10}," + + "{\"appraisedValue\":700,\"assetID\":\"asset5\",\"color\":\"black\",\"owner\":\"Adrian\",\"size\":15}," + + "{\"appraisedValue\":800,\"assetID\":\"asset6\",\"color\":\"white\",\"owner\":\"Michel\",\"size\":15}]"); + + } + + @Nested + class TransferAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }"); + + Asset asset = contract.TransferAsset(ctx, "asset1", "Dr Evil"); + + assertThat(asset).isEqualTo(new Asset("asset1", "blue", 5, "Dr Evil", 300)); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.TransferAsset(ctx, "asset1", "Dr Evil"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } + + @Nested + class UpdateAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 45, \"owner\": \"Arturo\", \"appraisedValue\": 60 }"); + + Asset asset = contract.UpdateAsset(ctx, "asset1", "pink", 45, "Arturo", 600); + + assertThat(asset).isEqualTo(new Asset("asset1", "pink", 45, "Arturo", 600)); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.TransferAsset(ctx, "asset1", "Alex"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } + + @Nested + class DeleteAssetTransaction { + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.DeleteAsset(ctx, "asset1"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } +} diff --git a/asset-transfer-basic/chaincode-javascript/.eslintignore b/asset-transfer-basic/chaincode-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/asset-transfer-basic/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-basic/chaincode-javascript/.eslintrc.js b/asset-transfer-basic/chaincode-javascript/.eslintrc.js new file mode 100644 index 0000000..555c0cf --- /dev/null +++ b/asset-transfer-basic/chaincode-javascript/.eslintrc.js @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + env: { + node: true, + mocha: true, + es6: 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/asset-transfer-basic/chaincode-javascript/.gitignore b/asset-transfer-basic/chaincode-javascript/.gitignore new file mode 100644 index 0000000..eeace29 --- /dev/null +++ b/asset-transfer-basic/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-basic/chaincode-javascript/index.js b/asset-transfer-basic/chaincode-javascript/index.js new file mode 100644 index 0000000..55a0dcf --- /dev/null +++ b/asset-transfer-basic/chaincode-javascript/index.js @@ -0,0 +1,12 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const assetTransfer = require('./lib/assetTransfer'); + +module.exports.AssetTransfer = assetTransfer; +module.exports.contracts = [assetTransfer]; diff --git a/asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js b/asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js new file mode 100644 index 0000000..d3a4fc4 --- /dev/null +++ b/asset-transfer-basic/chaincode-javascript/lib/assetTransfer.js @@ -0,0 +1,153 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Contract } = require('fabric-contract-api'); + +class AssetTransfer extends Contract { + + async InitLedger(ctx) { + const assets = [ + { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }, + { + ID: 'asset2', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }, + { + ID: 'asset3', + Color: 'green', + Size: 10, + Owner: 'Jin Soo', + AppraisedValue: 500, + }, + { + ID: 'asset4', + Color: 'yellow', + Size: 10, + Owner: 'Max', + AppraisedValue: 600, + }, + { + ID: 'asset5', + Color: 'black', + Size: 15, + Owner: 'Adriana', + AppraisedValue: 700, + }, + { + ID: 'asset6', + Color: 'white', + Size: 15, + Owner: 'Michel', + AppraisedValue: 800, + }, + ]; + + for (const asset of assets) { + asset.docType = 'asset'; + await ctx.stub.putState(asset.ID, Buffer.from(JSON.stringify(asset))); + console.info(`Asset ${asset.ID} initialized`); + } + } + + // 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, + }; + 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. + async ReadAsset(ctx, id) { + const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state + if (!assetJSON || assetJSON.length === 0) { + throw new Error(`The asset ${id} does not exist`); + } + return assetJSON.toString(); + } + + // UpdateAsset updates an existing asset in the world state with provided parameters. + async UpdateAsset(ctx, id, color, size, owner, appraisedValue) { + const exists = await this.AssetExists(ctx, id); + if (!exists) { + throw new Error(`The asset ${id} does not exist`); + } + + // overwriting original asset with new asset + const updatedAsset = { + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + }; + return ctx.stub.putState(id, Buffer.from(JSON.stringify(updatedAsset))); + } + + // DeleteAsset deletes an given asset from the world state. + async DeleteAsset(ctx, id) { + const exists = await this.AssetExists(ctx, id); + if (!exists) { + throw new Error(`The asset ${id} does not exist`); + } + return ctx.stub.deleteState(id); + } + + // AssetExists returns true when asset with given ID exists in world state. + async AssetExists(ctx, id) { + const assetJSON = await ctx.stub.getState(id); + return assetJSON && assetJSON.length > 0; + } + + // TransferAsset updates the owner field of asset with given id in the world state. + async TransferAsset(ctx, id, newOwner) { + const assetString = await this.ReadAsset(ctx, id); + const asset = JSON.parse(assetString); + asset.Owner = newOwner; + return ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); + } + + // GetAllAssets returns all assets found in the world state. + async GetAllAssets(ctx) { + const allResults = []; + // range query with empty string for startKey and endKey does an open-ended query of all assets in the chaincode namespace. + const iterator = await ctx.stub.getStateByRange('', ''); + let result = await iterator.next(); + while (!result.done) { + const strValue = Buffer.from(result.value.value.toString()).toString('utf8'); + let record; + try { + record = JSON.parse(strValue); + } catch (err) { + console.log(err); + record = strValue; + } + allResults.push({ Key: result.value.key, Record: record }); + result = await iterator.next(); + } + return JSON.stringify(allResults); + } + + +} + +module.exports = AssetTransfer; diff --git a/asset-transfer-basic/chaincode-javascript/package.json b/asset-transfer-basic/chaincode-javascript/package.json new file mode 100644 index 0000000..9cc2224 --- /dev/null +++ b/asset-transfer-basic/chaincode-javascript/package.json @@ -0,0 +1,49 @@ +{ + "name": "asset-transfer-basic", + "version": "1.0.0", + "description": "Asset-Transfer-Basic 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-basic/chaincode-javascript/test/assetTransfer.test.js b/asset-transfer-basic/chaincode-javascript/test/assetTransfer.test.js new file mode 100644 index 0000000..e9db032 --- /dev/null +++ b/asset-transfer-basic/chaincode-javascript/test/assetTransfer.test.js @@ -0,0 +1,262 @@ +'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 } = require('fabric-shim'); + +const AssetTransfer = require('../lib/assetTransfer.js'); + +let assert = sinon.assert; +chai.use(sinonChai); + +describe('Asset Transfer Basic Tests', () => { + let transactionContext, chaincodeStub, asset; + beforeEach(() => { + transactionContext = new Context(); + + chaincodeStub = sinon.createStubInstance(ChaincodeStub); + transactionContext.setChaincodeStub(chaincodeStub); + + 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, + }; + }); + + describe('Test InitLedger', () => { + it('should return error on InitLedger', async () => { + chaincodeStub.putState.rejects('failed inserting key'); + let assetTransfer = new AssetTransfer(); + try { + await assetTransfer.InitLedger(transactionContext); + assert.fail('InitLedger should have failed'); + } catch (err) { + expect(err.name).to.equal('failed inserting key'); + } + }); + + it('should return success on InitLedger', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.InitLedger(transactionContext); + let ret = JSON.parse((await chaincodeStub.getState('asset1')).toString()); + expect(ret).to.eql(Object.assign({docType: 'asset'}, asset)); + }); + }); + + 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); + }); + }); + + 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); + + let ret = JSON.parse(await chaincodeStub.getState(asset.ID)); + expect(ret).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'})); + }); + }); + + describe('Test GetAllAssets', () => { + it('should return success on GetAllAssets', async () => { + let assetTransfer = new AssetTransfer(); + + await assetTransfer.CreateAsset(transactionContext, 'asset1', 'blue', 5, 'Robert', 100); + await assetTransfer.CreateAsset(transactionContext, 'asset2', 'orange', 10, 'Paul', 200); + await assetTransfer.CreateAsset(transactionContext, 'asset3', 'red', 15, 'Troy', 300); + await assetTransfer.CreateAsset(transactionContext, 'asset4', 'pink', 20, 'Van', 400); + + let ret = await assetTransfer.GetAllAssets(transactionContext); + ret = JSON.parse(ret); + expect(ret.length).to.equal(4); + + let expected = [ + {Record: {ID: 'asset1', Color: 'blue', Size: 5, Owner: 'Robert', AppraisedValue: 100}}, + {Record: {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200}}, + {Record: {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300}}, + {Record: {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}} + ]; + + expect(ret).to.eql(expected); + }); + + it('should return success on GetAllAssets for non JSON value', async () => { + let assetTransfer = new AssetTransfer(); + + chaincodeStub.putState.onFirstCall().callsFake((key, value) => { + if (!chaincodeStub.states) { + chaincodeStub.states = {}; + } + chaincodeStub.states[key] = 'non-json-value'; + }); + + await assetTransfer.CreateAsset(transactionContext, 'asset1', 'blue', 5, 'Robert', 100); + await assetTransfer.CreateAsset(transactionContext, 'asset2', 'orange', 10, 'Paul', 200); + await assetTransfer.CreateAsset(transactionContext, 'asset3', 'red', 15, 'Troy', 300); + await assetTransfer.CreateAsset(transactionContext, 'asset4', 'pink', 20, 'Van', 400); + + let ret = await assetTransfer.GetAllAssets(transactionContext); + ret = JSON.parse(ret); + expect(ret.length).to.equal(4); + + let expected = [ + {Record: 'non-json-value'}, + {Record: {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200}}, + {Record: {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300}}, + {Record: {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}} + ]; + + expect(ret).to.eql(expected); + }); + }); +}); diff --git a/asset-transfer-basic/chaincode-typescript/.gitignore b/asset-transfer-basic/chaincode-typescript/.gitignore new file mode 100644 index 0000000..79bfe1a --- /dev/null +++ b/asset-transfer-basic/chaincode-typescript/.gitignore @@ -0,0 +1,16 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +# Compiled TypeScript files +dist + diff --git a/asset-transfer-basic/chaincode-typescript/package.json b/asset-transfer-basic/chaincode-typescript/package.json new file mode 100644 index 0000000..2b681fc --- /dev/null +++ b/asset-transfer-basic/chaincode-typescript/package.json @@ -0,0 +1,62 @@ +{ + "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", + "test": "nyc mocha -r ts-node/register src/**/*.spec.ts", + "start": "fabric-chaincode-node start", + "build": "tsc", + "build:watch": "tsc -w", + "prepublishOnly": "npm run build" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.5", + "@types/node": "^10.12.10", + "@types/sinon": "^5.0.7", + "@types/sinon-chai": "^3.2.1", + "chai": "^4.2.0", + "mocha": "^5.2.0", + "nyc": "^14.1.1", + "sinon": "^7.1.1", + "sinon-chai": "^3.3.0", + "ts-node": "^7.0.1", + "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/chaincode-typescript/src/asset.ts b/asset-transfer-basic/chaincode-typescript/src/asset.ts new file mode 100644 index 0000000..9fd62d3 --- /dev/null +++ b/asset-transfer-basic/chaincode-typescript/src/asset.ts @@ -0,0 +1,26 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ + +import {Object, Property} from 'fabric-contract-api'; + +@Object() +export class Asset { + @Property() + public docType?: string; + + @Property() + public ID: string; + + @Property() + public Color: string; + + @Property() + public Size: number; + + @Property() + public Owner: string; + + @Property() + public AppraisedValue: number; +} diff --git a/asset-transfer-basic/chaincode-typescript/src/assetTransfer.ts b/asset-transfer-basic/chaincode-typescript/src/assetTransfer.ts new file mode 100644 index 0000000..b21f12e --- /dev/null +++ b/asset-transfer-basic/chaincode-typescript/src/assetTransfer.ts @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Context, Contract, Info, Returns, Transaction} from 'fabric-contract-api'; +import {Asset} from './asset'; + +@Info({title: 'AssetTransfer', description: 'Smart contract for trading assets'}) +export class AssetTransferContract extends Contract { + + @Transaction() + public async InitLedger(ctx: Context): Promise { + const assets: Asset[] = [ + { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }, + { + ID: 'asset2', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }, + { + ID: 'asset3', + Color: 'green', + Size: 10, + Owner: 'Jin Soo', + AppraisedValue: 500, + }, + { + ID: 'asset4', + Color: 'yellow', + Size: 10, + Owner: 'Max', + AppraisedValue: 600, + }, + { + ID: 'asset5', + Color: 'black', + Size: 15, + Owner: 'Adriana', + AppraisedValue: 700, + }, + { + ID: 'asset6', + Color: 'white', + Size: 15, + Owner: 'Michel', + AppraisedValue: 800, + }, + ]; + + for (const asset of assets) { + asset.docType = 'asset'; + await ctx.stub.putState(asset.ID, Buffer.from(JSON.stringify(asset))); + console.info(`Asset ${asset.ID} initialized`); + } + } + + // CreateAsset issues a new asset to the world state with given details. + @Transaction() + public async CreateAsset(ctx: Context, id: string, color: string, size: number, owner: string, appraisedValue: number): Promise { + const asset = { + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + }; + await ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); + } + + // ReadAsset returns the asset stored in the world state with given id. + @Transaction(false) + public async ReadAsset(ctx: Context, id: string): Promise { + const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state + if (!assetJSON || assetJSON.length === 0) { + throw new Error(`The asset ${id} does not exist`); + } + return assetJSON.toString(); + } + + // UpdateAsset updates an existing asset in the world state with provided parameters. + @Transaction() + public async UpdateAsset(ctx: Context, id: string, color: string, size: number, owner: string, appraisedValue: number): Promise { + const exists = await this.AssetExists(ctx, id); + if (!exists) { + throw new Error(`The asset ${id} does not exist`); + } + + // overwriting original asset with new asset + const updatedAsset = { + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + }; + return ctx.stub.putState(id, Buffer.from(JSON.stringify(updatedAsset))); + } + + // DeleteAsset deletes an given asset from the world state. + @Transaction() + public async DeleteAsset(ctx: Context, id: string): Promise { + const exists = await this.AssetExists(ctx, id); + if (!exists) { + throw new Error(`The asset ${id} does not exist`); + } + return ctx.stub.deleteState(id); + } + + // AssetExists returns true when asset with given ID exists in world state. + @Transaction(false) + @Returns('boolean') + public async AssetExists(ctx: Context, id: string): Promise { + const assetJSON = await ctx.stub.getState(id); + return assetJSON && assetJSON.length > 0; + } + + // TransferAsset updates the owner field of asset with given id in the world state. + @Transaction() + public async TransferAsset(ctx: Context, id: string, newOwner: string): Promise { + const assetString = await this.ReadAsset(ctx, id); + const asset = JSON.parse(assetString); + asset.Owner = newOwner; + await ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); + } + + // GetAllAssets returns all assets found in the world state. + @Transaction(false) + @Returns('string') + public async GetAllAssets(ctx: Context): Promise { + const allResults = []; + // range query with empty string for startKey and endKey does an open-ended query of all assets in the chaincode namespace. + const iterator = await ctx.stub.getStateByRange('', ''); + let result = await iterator.next(); + while (!result.done) { + const strValue = Buffer.from(result.value.value.toString()).toString('utf8'); + let record; + try { + record = JSON.parse(strValue); + } catch (err) { + console.log(err); + record = strValue; + } + allResults.push({Key: result.value.key, Record: record}); + result = await iterator.next(); + } + return JSON.stringify(allResults); + } + +} diff --git a/asset-transfer-basic/chaincode-typescript/src/index.ts b/asset-transfer-basic/chaincode-typescript/src/index.ts new file mode 100644 index 0000000..cdd7b58 --- /dev/null +++ b/asset-transfer-basic/chaincode-typescript/src/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import {AssetTransferContract} from './assetTransfer'; + +export {AssetTransferContract} from './assetTransfer'; + +export const contracts: any[] = [AssetTransferContract]; diff --git a/asset-transfer-basic/chaincode-typescript/tsconfig.json b/asset-transfer-basic/chaincode-typescript/tsconfig.json new file mode 100644 index 0000000..80d8e12 --- /dev/null +++ b/asset-transfer-basic/chaincode-typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "outDir": "dist", + "target": "es2017", + "moduleResolution": "node", + "module": "commonjs", + "declaration": true, + "sourceMap": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/asset-transfer-basic/chaincode-typescript/tslint.json b/asset-transfer-basic/chaincode-typescript/tslint.json new file mode 100644 index 0000000..a52c3ee --- /dev/null +++ b/asset-transfer-basic/chaincode-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/asset-transfer-events/README.md b/asset-transfer-events/README.md new file mode 100644 index 0000000..34e90bd --- /dev/null +++ b/asset-transfer-events/README.md @@ -0,0 +1,91 @@ +# Asset Transfer Events Sample + +The asset transfer events sample demonstrates chaincode events send/receive +and the receive of block events. The chaincode events are set by your +chaincode which adds the event data to the transaction and are sent when the +transaction is committed to the ledger. The block events are published when +a block is committed to the ledger, containing all the transaction details +within that block. + +For more information about event services on per-channel basis, visit the +[Channel-based event service](https://hyperledger-fabric.readthedocs.io/en/latest/peer_event_services.html) +page in the Fabric documentation. + + +## About the Sample + +This sample includes chaincodes and application code in multiple languages. +In a use-case similar to basic asset transfer ( see `../asset-transfer-basic` folder) +this sample shows sending and receiving of events during create/update/delete of an asset +and during transfer of an asset to a new owner. + +### Application +The application demonstrates this, using two types of listeners in subsequent sections of `main` function: +1. Contract Listener: listen for events in a specific Contract +- How to register a contract listener in an application, 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 + +2. Block Listener: listen for block level events and parse private-data events +- How to register a block listener for full block events +- How to retrieve the transaction and block information from the block event +- How to register to receive private data associated with transactions, when registering a block listener +- How to retrieve the private data collection details from the full block event +- This section also shows how to connect to a Gateway with listener that will not listen for commit events. This may be useful when the application does not want to wait for the peer to commit blocks and notify the application. + + +Follow the comments in `application-javascript/app.js` file, and corresponding output on running this application. +Pay attention to the sequence of +- smart contract calls (console output like `--> Submit Transaction or --> Evaluate`) +- the events received at application end (console output like `<-- Contract Event Received: or <-- Block Event Received`) + +The listener will be notified of an event asynchronously. Notice that events will +be posted by the listener after the application code sends the transaction (or after the +change is committed to the ledger), but during other application activity unrelated to the event. + +### Smart Contract +The smart contract implements (in folder `chaincode-xyz`) following functions to support the application: +- CreateAsset +- ReadAsset +- UpdateAsset +- DeleteAsset +- TransferAsset + +Note that the asset transfer implemented by the smart contract is a simplified scenario, without ownership validation, meant only to +demonstrate the use of sending and receiving events. + + +## Running the sample + +Like other samples, we will use the Fabric test network to deploy and run ths sample. Follow these step in order. +- Create the test network and a channel +``` +cd test-network +./network.sh up createChannel -c mychannel -ca +``` + +- Deploy the chaincode (smart contract) +``` +# to deploy javascript version +./network.sh deployCC -ccs 1 -ccv 1 -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -ccl javascript -ccp ./../asset-transfer-events/chaincode-javascript/ -ccn asset-transfer-events-javascript + +# or to deploy java version +./network.sh deployCC -ccs 1 -ccv 1 -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -ccl java -ccp ./../asset-transfer-events/chaincode-java/ -ccn asset-transfer-events-java +``` + +- Run the application +``` +cd application-javascript +npm install +# ensure this line in app.js have correct chaincode deploy name +# const chaincodeName = '...'; +node app.js +``` + + +## 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 +``` diff --git a/asset-transfer-events/application-javascript/.eslintignore b/asset-transfer-events/application-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /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 0000000..8422f8a --- /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 0000000..21b287f --- /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 0000000..4a8a1c2 --- /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 -ccp ../asset-transfer-events/chaincode-javacript/ -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 0000000..2767aed --- /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-java/build.gradle b/asset-transfer-events/chaincode-java/build.gradle new file mode 100644 index 0000000..f68178d --- /dev/null +++ b/asset-transfer-events/chaincode-java/build.gradle @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'application' + id 'checkstyle' + id 'jacoco' +} + +group 'org.hyperledger.fabric.samples' +version '1.0-SNAPSHOT' + +dependencies { + compileOnly 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' +} + +repositories { + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + jcenter() + maven { + url 'https://jitpack.io' + } +} + +application { + mainClassName = 'org.hyperledger.fabric.contract.ContractRouter' +} + + +checkstyle { + toolVersion '8.21' + configFile file("config/checkstyle/checkstyle.xml") +} + +checkstyleMain { + source ='src/main/java' +} + +checkstyleTest { + source ='src/test/java' +} + +jacocoTestReport { + dependsOn test +} + +installDist.dependsOn check \ No newline at end of file diff --git a/asset-transfer-events/chaincode-java/config/checkstyle/checkstyle.xml b/asset-transfer-events/chaincode-java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..acd5df4 --- /dev/null +++ b/asset-transfer-events/chaincode-java/config/checkstyle/checkstyle.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/asset-transfer-events/chaincode-java/config/checkstyle/suppressions.xml b/asset-transfer-events/chaincode-java/config/checkstyle/suppressions.xml new file mode 100644 index 0000000..33dda04 --- /dev/null +++ b/asset-transfer-events/chaincode-java/config/checkstyle/suppressions.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/asset-transfer-events/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-events/chaincode-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f3d88b1c2faf2fc91d853cd5d4242b5547257070 GIT binary patch literal 58695 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 literal 0 HcmV?d00001 diff --git a/asset-transfer-events/chaincode-java/gradle/wrapper/gradle-wrapper.properties b/asset-transfer-events/chaincode-java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1b16c34 --- /dev/null +++ b/asset-transfer-events/chaincode-java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/asset-transfer-events/chaincode-java/gradlew b/asset-transfer-events/chaincode-java/gradlew new file mode 100755 index 0000000..2fe81a7 --- /dev/null +++ b/asset-transfer-events/chaincode-java/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/asset-transfer-events/chaincode-java/gradlew.bat b/asset-transfer-events/chaincode-java/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/asset-transfer-events/chaincode-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/asset-transfer-events/chaincode-java/settings.gradle b/asset-transfer-events/chaincode-java/settings.gradle new file mode 100644 index 0000000..7b5f82c --- /dev/null +++ b/asset-transfer-events/chaincode-java/settings.gradle @@ -0,0 +1,5 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'asset-transfer-events-java' diff --git a/asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/Asset.java b/asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/Asset.java new file mode 100644 index 0000000..f9bdc18 --- /dev/null +++ b/asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/Asset.java @@ -0,0 +1,158 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.events; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + + +import org.json.JSONObject; + +@DataType() +public final class Asset { + + @Property() + private final String assetID; + + @Property() + private String color; + + @Property() + private int size; + + @Property() + private String owner; + + @Property() + private int appraisedValue; + + public Asset(final String assetID, final String color, + final int size, final String owner, final int value) { + + this.assetID = assetID; + this.color = color; + this.size = size; + this.owner = owner; + this.appraisedValue = value; + } + + public String getAssetID() { + return assetID; + } + + public String getColor() { + return color; + } + + public int getSize() { + return size; + } + + public String getOwner() { + return owner; + } + + public int getAppraisedValue() { + return appraisedValue; + } + + public void setOwner(final String newowner) { + this.owner = newowner; + } + + public void setAppraisedValue(final int value) { + this.appraisedValue = value; + } + + public void setColor(final String c) { + this.color = c; + } + + public void setSize(final int s) { + this.size = s; + } + + // Serialize asset without private properties + public byte[] serialize() { + return serialize(null).getBytes(UTF_8); + } + + public String serialize(final String privateProps) { + Map tMap = new HashMap(); + tMap.put("ID", assetID); + tMap.put("Color", color); + tMap.put("Owner", owner); + tMap.put("Size", Integer.toString(size)); + tMap.put("AppraisedValue", Integer.toString(appraisedValue)); + if (privateProps != null && privateProps.length() > 0) { + tMap.put("asset_properties", new JSONObject(privateProps)); + } + return new JSONObject(tMap).toString(); + } + + public static Asset deserialize(final byte[] assetJSON) { + return deserialize(new String(assetJSON, UTF_8)); + } + + public static Asset deserialize(final String assetJSON) { + + JSONObject json = new JSONObject(assetJSON); + Map tMap = json.toMap(); + final String id = (String) tMap.get("ID"); + + final String color = (String) tMap.get("Color"); + final String owner = (String) tMap.get("Owner"); + int size = 0; + int appraisedValue = 0; + if (tMap.containsKey("Size")) { + size = Integer.parseInt((String) tMap.get("Size")); + } + if (tMap.containsKey("AppraisedValue")) { + appraisedValue = Integer.parseInt((String) tMap.get("AppraisedValue")); + } + return new Asset(id, color, size, owner, appraisedValue); + + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + Asset other = (Asset) obj; + + return Objects.deepEquals( + new String[]{getAssetID(), getColor(), getOwner()}, + new String[]{other.getAssetID(), other.getColor(), other.getOwner()}) + && + Objects.deepEquals( + new int[]{getSize(), getAppraisedValue()}, + new int[]{other.getSize(), other.getAppraisedValue()}); + } + + @Override + public int hashCode() { + return Objects.hash(getAssetID(), getColor(), getSize(), getOwner(), getAppraisedValue()); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + + " [assetID=" + assetID + ", appraisedValue=" + appraisedValue + ", color=" + + color + ", size=" + size + ", owner=" + owner + "]"; + } + +} diff --git a/asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/AssetTransfer.java b/asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/AssetTransfer.java new file mode 100644 index 0000000..536167a --- /dev/null +++ b/asset-transfer-events/chaincode-java/src/main/java/org/hyperledger/fabric/samples/events/AssetTransfer.java @@ -0,0 +1,299 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.events; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contact; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; + +import java.util.Map; + +/** + * Main Chaincode class. + * + * @see org.hyperledger.fabric.shim.Chaincode + *

+ * Each chaincode transaction function must take, Context as first parameter. + * Unless specified otherwise via annotation (@Contract or @Transaction), the contract name + * is the class name (without package) + * and the transaction name is the method name. + */ +@Contract( + name = "asset-transfer-events-java", + info = @Info( + title = "Asset Transfer Events", + description = "The hyperlegendary asset transfer events sample", + version = "0.0.1-SNAPSHOT", + license = @License( + name = "Apache 2.0 License", + url = "http://www.apache.org/licenses/LICENSE-2.0.html"), + contact = @Contact( + email = "a.transfer@example.com", + name = "Fabric Development Team", + url = "https://hyperledger.example.com"))) +@Default +public final class AssetTransfer implements ContractInterface { + + static final String IMPLICIT_COLLECTION_NAME_PREFIX = "_implicit_org_"; + static final String PRIVATE_PROPS_KEY = "asset_properties"; + + /** + * Retrieves the asset details with the specified ID + * + * @param ctx the transaction context + * @param assetID the ID of the asset + * @return the asset found on the ledger. Returns error if asset is not found + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String ReadAsset(final Context ctx, final String assetID) { + System.out.printf("ReadAsset: ID %s\n", assetID); + + Asset asset = getState(ctx, assetID); + String privData = readPrivateData(ctx, assetID); + return asset.serialize(privData); + } + + /** + * Creates a new asset on the ledger. Saves the passed private data (asset properties) from transient map input. + * + * @param ctx the transaction context + * Transient map with asset_properties key with asset json as value + * @param assetID + * @param color + * @param size + * @param owner + * @param appraisedValue + * @return the created asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset CreateAsset(final Context ctx, final String assetID, final String color, final int size, final String owner, final int appraisedValue) { + ChaincodeStub stub = ctx.getStub(); + //input validations + String errorMessage = null; + if (assetID == null || assetID.equals("")) { + errorMessage = String.format("Empty input: assetID"); + } + if (owner == null || owner.equals("")) { + errorMessage = String.format("Empty input: owner"); + } + + if (errorMessage != null) { + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + // Check if asset already exists + byte[] assetJSON = ctx.getStub().getState(assetID); + if (assetJSON != null && assetJSON.length > 0) { + errorMessage = String.format("Asset %s already exists", assetID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString()); + } + + Asset asset = new Asset(assetID, color, size, owner, appraisedValue); + + savePrivateData(ctx, assetID); + assetJSON = asset.serialize(); + System.out.printf("CreateAsset Put: ID %s Data %s\n", assetID, new String(assetJSON)); + + stub.putState(assetID, assetJSON); + // add Event data to the transaction data. Event will be published after the block containing + // this transaction is committed + stub.setEvent("CreateAsset", assetJSON); + return asset; + } + + + /** + * TransferAsset transfers the asset to the new owner + * Save any private data, if provided in transient map + * + * @param ctx the transaction context + * @param assetID asset to delete + * @param newOwner new owner for the asset + * @return none + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void TransferAsset(final Context ctx, final String assetID, final String newOwner) { + ChaincodeStub stub = ctx.getStub(); + String errorMessage = null; + + if (assetID == null || assetID.equals("")) { + errorMessage = "Empty input: assetID"; + } + if (newOwner == null || newOwner.equals("")) { + errorMessage = "Empty input: newOwner"; + } + if (errorMessage != null) { + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + System.out.printf("TransferAsset: verify asset %s exists\n", assetID); + Asset thisAsset = getState(ctx, assetID); + // Transfer asset to new owner + thisAsset.setOwner(newOwner); + + System.out.printf(" Transfer Asset: ID %s to owner %s\n", assetID, newOwner); + savePrivateData(ctx, assetID); // save private data if any + byte[] assetJSON = thisAsset.serialize(); + + stub.putState(assetID, assetJSON); + stub.setEvent("TransferAsset", assetJSON); //publish Event + } + + /** + * Update existing asset on the ledger with provided parameters. + * Saves the passed private data (asset properties) from transient map input. + * + * @param ctx the transaction context + * Transient map with asset_properties key with asset json as value + * @param assetID + * @param color + * @param size + * @param owner + * @param appraisedValue + * @return the created asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset UpdateAsset(final Context ctx, final String assetID, final String color, final int size, final String owner, final int appraisedValue) { + ChaincodeStub stub = ctx.getStub(); + //input validations + String errorMessage = null; + if (assetID == null || assetID.equals("")) { + errorMessage = String.format("Empty input: assetID"); + } + + if (errorMessage != null) { + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + // reads from the Statedb. Check if asset already exists + Asset asset = getState(ctx, assetID); + + if (owner != null) { + asset.setOwner(owner); + } + if (color != null) { + asset.setColor(color); + } + if (size > 0) { + asset.setSize(size); + } + if (appraisedValue > 0) { + asset.setAppraisedValue(appraisedValue); + } + + savePrivateData(ctx, assetID); + byte[] assetJSON = asset.serialize(); + System.out.printf("UpdateAsset Put: ID %s Data %s\n", assetID, new String(assetJSON)); + stub.putState(assetID, assetJSON); + stub.setEvent("UpdateAsset", assetJSON); //publish Event + return asset; + } + + /** + * Deletes a asset & related details from the ledger. + * + * @param ctx the transaction context + * @param assetID asset to delete + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void DeleteAsset(final Context ctx, final String assetID) { + ChaincodeStub stub = ctx.getStub(); + System.out.printf("DeleteAsset: verify asset %s exists\n", assetID); + Asset asset = getState(ctx, assetID); + + System.out.printf(" DeleteAsset: ID %s\n", assetID); + // delete private details of asset + removePrivateData(ctx, assetID); + stub.delState(assetID); // delete the key from Statedb + stub.setEvent("DeleteAsset", asset.serialize()); //publish Event + } + + private Asset getState(final Context ctx, final String assetID) { + byte[] assetJSON = ctx.getStub().getState(assetID); + if (assetJSON == null || assetJSON.length == 0) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + try { + Asset asset = Asset.deserialize(assetJSON); + return asset; + } catch (Exception e) { + throw new ChaincodeException("Deserialize error: " + e.getMessage(), AssetTransferErrors.DATA_ERROR.toString()); + } + } + + private String readPrivateData(final Context ctx, final String assetKey) { + String peerMSPID = ctx.getStub().getMspId(); + String clientMSPID = ctx.getClientIdentity().getMSPID(); + String implicitCollectionName = getCollectionName(ctx); + String privData = null; + //only if ClientOrgMatchesPeerOrg + if (peerMSPID.equals(clientMSPID)) { + System.out.printf(" ReadPrivateData from collection %s, ID %s\n", implicitCollectionName, assetKey); + byte[] propJSON = ctx.getStub().getPrivateData(implicitCollectionName, assetKey); + + if (propJSON != null && propJSON.length > 0) { + privData = new String(propJSON, UTF_8); + } + } + return privData; + } + + private void savePrivateData(final Context ctx, final String assetKey) { + String peerMSPID = ctx.getStub().getMspId(); + String clientMSPID = ctx.getClientIdentity().getMSPID(); + String implicitCollectionName = getCollectionName(ctx); + + if (peerMSPID.equals(clientMSPID)) { + Map transientMap = ctx.getStub().getTransient(); + if (transientMap != null && transientMap.containsKey(PRIVATE_PROPS_KEY)) { + byte[] transientAssetJSON = transientMap.get(PRIVATE_PROPS_KEY); + + System.out.printf("Asset's PrivateData Put in collection %s, ID %s\n", implicitCollectionName, assetKey); + ctx.getStub().putPrivateData(implicitCollectionName, assetKey, transientAssetJSON); + } + } + } + + private void removePrivateData(final Context ctx, final String assetKey) { + String peerMSPID = ctx.getStub().getMspId(); + String clientMSPID = ctx.getClientIdentity().getMSPID(); + String implicitCollectionName = getCollectionName(ctx); + + if (peerMSPID.equals(clientMSPID)) { + System.out.printf("PrivateData Delete from collection %s, ID %s\n", implicitCollectionName, assetKey); + ctx.getStub().delPrivateData(implicitCollectionName, assetKey); + } + } + + // Return the implicit collection name, to use for private property persistance + private String getCollectionName(final Context ctx) { + // Get the MSP ID of submitting client identity + String clientMSPID = ctx.getClientIdentity().getMSPID(); + String collectionName = IMPLICIT_COLLECTION_NAME_PREFIX + clientMSPID; + return collectionName; + } + + private enum AssetTransferErrors { + INCOMPLETE_INPUT, + INVALID_ACCESS, + ASSET_NOT_FOUND, + ASSET_ALREADY_EXISTS, + DATA_ERROR + } + +} diff --git a/asset-transfer-events/chaincode-javascript/.eslintignore b/asset-transfer-events/chaincode-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /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 0000000..cb00fa9 --- /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 0000000..eeace29 --- /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 0000000..3244ced --- /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 0000000..27c5acb --- /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 0000000..143b35c --- /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 0000000..b243cdb --- /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/asset-transfer-ledger-queries/application-java/.gitattributes b/asset-transfer-ledger-queries/application-java/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/asset-transfer-ledger-queries/application-java/build.gradle b/asset-transfer-ledger-queries/application-java/build.gradle new file mode 100644 index 0000000..e2eed62 --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/build.gradle @@ -0,0 +1,43 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java project to get you started. + * For more details take a look at the Java Quickstart chapter in the Gradle + * User Manual available at https://docs.gradle.org/6.5/userguide/tutorial_java_projects.html + */ + +plugins { + // Apply the java plugin to add support for Java + id 'java' + + // Apply the application plugin to add support for building a CLI application. + id 'application' +} +ext { + javaMainClass = "application.java.App" +} + +repositories { + // Use jcenter for resolving dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} + +dependencies { + // This dependency is used by the application. + implementation 'com.google.guava:guava:29.0-jre' + implementation 'org.hyperledger.fabric:fabric-gateway-java:2.1.1' +} + +application { + // Define the main class for the application. + mainClassName = 'application.java.App' +} + +// task for running the app after building dependencies +task runApp(type: Exec) { + dependsOn build + group = "Execution" + description = "Run the main class with ExecTask" + commandLine "java", "-classpath", sourceSets.main.runtimeClasspath.getAsPath(), javaMainClass +} \ No newline at end of file diff --git a/asset-transfer-ledger-queries/application-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-ledger-queries/application-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/asset-transfer-ledger-queries/application-java/gradle/wrapper/gradle-wrapper.properties b/asset-transfer-ledger-queries/application-java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..622ab64 --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/asset-transfer-ledger-queries/application-java/gradlew b/asset-transfer-ledger-queries/application-java/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/asset-transfer-ledger-queries/application-java/gradlew.bat b/asset-transfer-ledger-queries/application-java/gradlew.bat new file mode 100644 index 0000000..5093609 --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/asset-transfer-ledger-queries/application-java/settings.gradle b/asset-transfer-ledger-queries/application-java/settings.gradle new file mode 100644 index 0000000..5423bc7 --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.5/userguide/multi_project_builds.html + */ + +rootProject.name = 'application-java' diff --git a/asset-transfer-ledger-queries/application-java/src/main/java/application/java/App.java b/asset-transfer-ledger-queries/application-java/src/main/java/application/java/App.java new file mode 100644 index 0000000..2a1b74c --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/src/main/java/application/java/App.java @@ -0,0 +1,142 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// Running TestApp: +// gradle runApp + +package application.java; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.hyperledger.fabric.gateway.Contract; +import org.hyperledger.fabric.gateway.Gateway; +import org.hyperledger.fabric.gateway.Network; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; + + +public class App { + + static { + System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true"); + } + + // helper function for getting connected to the gateway + public static Gateway connect() throws Exception{ + // Load a file system based wallet for managing identities. + Path walletPath = Paths.get("wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + // load a CCP + Path networkConfigPath = Paths.get("..", "..", "test-network", "organizations", "peerOrganizations", "org1.example.com", "connection-org1.yaml"); + + Gateway.Builder builder = Gateway.createBuilder(); + builder.identity(wallet, "appUser").networkConfig(networkConfigPath).discovery(true); + return builder.connect(); + } + + public static void main(String[] args) throws Exception { + // enrolls the admin and registers the user + try { + EnrollAdmin.main(null); + RegisterUser.main(null); + } catch (Exception e) { + System.err.println(e); + } + + // connect to the network and invoke the smart contract + try (Gateway gateway = connect()) { + + // get the network and contract + Network network = gateway.getNetwork("mychannel"); + Contract contract = network.getContract("ledger"); + + byte[] result; + + System.out.println("Submit Transaction: InitLedger creates the initial set of assets on the ledger."); + contract.submitTransaction("InitLedger"); + + System.out.println("\n"); + // passing in 2 empty strings will query all the assets + result = contract.evaluateTransaction("GetAssetsByRange", "", ""); + System.out.println("Evaluate Transaction: GetAssetsByRange, result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Submit Transaction: CreateAsset asset13"); + //CreateAsset creates an asset with ID asset13, color yellow, owner Tom, size 5 and appraisedValue of 1300 + contract.submitTransaction("CreateAsset", "asset13", "yellow", "5", "Tom", "1300"); + + System.out.println("\n"); + System.out.println("Evaluate Transaction: ReadAsset asset13"); + // ReadAsset returns an asset with given assetID + result = contract.evaluateTransaction("ReadAsset", "asset13"); + System.out.println("result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Evaluate Transaction: AssetExists asset1"); + // AssetExists returns "true" if an asset with given assetID exist + result = contract.evaluateTransaction("AssetExists", "asset1"); + System.out.println("result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Submit Transaction: DeleteAsset asset1"); + contract.submitTransaction("DeleteAsset", "asset1"); + + System.out.println("\n"); + System.out.println("Evaluate Transaction: AssetExists asset1"); + // AssetExists returns "true" if an asset with given assetID exist + result = contract.evaluateTransaction("AssetExists", "asset1"); + System.out.println("result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Submit Transaction: TransferAsset asset2 from owner Tomoko > owner Tom"); + // TransferAsset transfers an asset with given ID to new owner Tom + contract.submitTransaction("TransferAsset", "asset2", "Tom"); + + // Rich Query with Pagination (Only supported if CouchDB is used as state database) + System.out.println("\n"); + System.out.println("Evaluate Transaction:QueryAssetsWithPagination Tom's assets"); + result = contract.evaluateTransaction("QueryAssetsWithPagination","{\"selector\":{\"docType\":\"asset\",\"owner\":\"Tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3",""); + System.out.println("result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Submit Transaction: TransferAssetByColor yellow assets > newOwner Michel"); + contract.submitTransaction("TransferAssetByColor", "yellow", "Michel"); + + // Rich Query (Only supported if CouchDB is used as state database): + System.out.println("\n"); + System.out.println("Evaluate Transaction:QueryAssetsByOwner Michel"); + result = contract.evaluateTransaction("QueryAssetsByOwner", "Michel"); + System.out.println("result: " + new String(result)); + + System.out.println("\n"); + System.out.println("Evaluate Transaction:GetAssetHistory asset13"); + result = contract.evaluateTransaction("GetAssetHistory", "asset13"); + System.out.println("result: " + new String(result)); + + // Rich Query (Only supported if CouchDB is used as state database): + System.out.println("\n"); + System.out.println("Evaluate Transaction:QueryAssets assets of size 15"); + result = contract.evaluateTransaction("QueryAssets", "{\"selector\":{\"size\":15}}"); + System.out.println("result: " + new String(result)); + + // Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database): + System.out.println("\n"); + System.out.println("Evaluate Transaction:QueryAssets Jin Soo's assets"); + result = contract.evaluateTransaction("QueryAssets","{\"selector\":{\"docType\":\"asset\",\"owner\":\"Jin Soo\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"); + System.out.println("result: " + new String(result)); + + // Rich Query with Pagination (Only supported if CouchDB is used as state database) + System.out.println("\n"); + System.out.println("Evaluate Transaction:GetAssetsByRangeWithPagination assets 3-5"); + result = contract.evaluateTransaction("GetAssetsByRangeWithPagination", "asset3", "asset6", "3",""); + System.out.println("result: " + new String(result)); + } + catch(Exception e){ + System.err.println(e); + } + + } +} diff --git a/asset-transfer-ledger-queries/application-java/src/main/java/application/java/EnrollAdmin.java b/asset-transfer-ledger-queries/application-java/src/main/java/application/java/EnrollAdmin.java new file mode 100644 index 0000000..563a35f --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/src/main/java/application/java/EnrollAdmin.java @@ -0,0 +1,53 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package application.java; + +import java.nio.file.Paths; +import java.util.Properties; + +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Identity; +import org.hyperledger.fabric.sdk.Enrollment; +import org.hyperledger.fabric.sdk.security.CryptoSuite; +import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory; +import org.hyperledger.fabric_ca.sdk.EnrollmentRequest; +import org.hyperledger.fabric_ca.sdk.HFCAClient; + +public class EnrollAdmin { + + public static void main(String[] args) throws Exception { + + // Create a CA client for interacting with the CA. + Properties props = new Properties(); + props.put("pemFile", + "../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem"); + props.put("allowAllHostNames", "true"); + HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props); + CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite(); + caClient.setCryptoSuite(cryptoSuite); + + // Create a wallet for managing identities + Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet")); + + // Check to see if we've already enrolled the admin user. + if (wallet.get("admin") != null) { + System.out.println("An identity for the admin user \"admin\" already exists in the wallet"); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + final EnrollmentRequest enrollmentRequestTLS = new EnrollmentRequest(); + enrollmentRequestTLS.addHost("localhost"); + enrollmentRequestTLS.setProfile("tls"); + Enrollment enrollment = caClient.enroll("admin", "adminpw", enrollmentRequestTLS); + Identity user = Identities.newX509Identity("Org1MSP", enrollment); + wallet.put("admin", user); + System.out.println("Successfully enrolled user \"admin\" and imported it into the wallet"); + } +} diff --git a/asset-transfer-ledger-queries/application-java/src/main/java/application/java/RegisterUser.java b/asset-transfer-ledger-queries/application-java/src/main/java/application/java/RegisterUser.java new file mode 100644 index 0000000..367b4a3 --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/src/main/java/application/java/RegisterUser.java @@ -0,0 +1,107 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package application.java; + +import java.nio.file.Paths; +import java.security.PrivateKey; +import java.util.Properties; +import java.util.Set; + +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Identity; +import org.hyperledger.fabric.gateway.X509Identity; +import org.hyperledger.fabric.sdk.Enrollment; +import org.hyperledger.fabric.sdk.User; +import org.hyperledger.fabric.sdk.security.CryptoSuite; +import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory; +import org.hyperledger.fabric_ca.sdk.HFCAClient; +import org.hyperledger.fabric_ca.sdk.RegistrationRequest; + +public class RegisterUser { + + public static void main(String[] args) throws Exception { + + // Create a CA client for interacting with the CA. + Properties props = new Properties(); + props.put("pemFile", + "../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem"); + props.put("allowAllHostNames", "true"); + HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props); + CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite(); + caClient.setCryptoSuite(cryptoSuite); + + // Create a wallet for managing identities + Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet")); + + // Check to see if we've already enrolled the user. + if (wallet.get("appUser") != null) { + System.out.println("An identity for the user \"appUser\" already exists in the wallet"); + return; + } + + X509Identity adminIdentity = (X509Identity)wallet.get("admin"); + if (adminIdentity == null) { + System.out.println("\"admin\" needs to be enrolled and added to the wallet first"); + return; + } + User admin = new User() { + + @Override + public String getName() { + return "admin"; + } + + @Override + public Set getRoles() { + return null; + } + + @Override + public String getAccount() { + return null; + } + + @Override + public String getAffiliation() { + return "org1.department1"; + } + + @Override + public Enrollment getEnrollment() { + return new Enrollment() { + + @Override + public PrivateKey getKey() { + return adminIdentity.getPrivateKey(); + } + + @Override + public String getCert() { + return Identities.toPemString(adminIdentity.getCertificate()); + } + }; + } + + @Override + public String getMspId() { + return "Org1MSP"; + } + + }; + + // Register the user, enroll the user, and import the new identity into the wallet. + RegistrationRequest registrationRequest = new RegistrationRequest("appUser"); + registrationRequest.setAffiliation("org1.department1"); + registrationRequest.setEnrollmentID("appUser"); + String enrollmentSecret = caClient.register(registrationRequest, admin); + Enrollment enrollment = caClient.enroll("appUser", enrollmentSecret); + Identity user = Identities.newX509Identity("Org1MSP", enrollment); + wallet.put("appUser", user); + System.out.println("Successfully enrolled user \"appUser\" and imported it into the wallet"); + } + +} diff --git a/asset-transfer-ledger-queries/application-java/src/main/resources/log4j.properties b/asset-transfer-ledger-queries/application-java/src/main/resources/log4j.properties new file mode 100644 index 0000000..f1f841f --- /dev/null +++ b/asset-transfer-ledger-queries/application-java/src/main/resources/log4j.properties @@ -0,0 +1,19 @@ +# initialize root logger with level ERROR for stdout and fout +log4j.rootLogger=ERROR,stdout,fout +# set the log level for these components +log4j.logger.com.endeca=INFO +log4j.logger.com.endeca.itl.web.metrics=INFO + +# add a ConsoleAppender to the logger stdout to write to the console +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +# use a simple message format +log4j.appender.stdout.layout.ConversionPattern=%m%n + +# add a FileAppender to the logger fout +log4j.appender.fout=org.apache.log4j.FileAppender +# create a log file +log4j.appender.fout.File=crawl.log +log4j.appender.fout.layout=org.apache.log4j.PatternLayout +# use a more detailed message pattern +log4j.appender.fout.layout.ConversionPattern=%p\t%d{ISO8601}\t%r\t%c\t[%t]\t%m%n diff --git a/asset-transfer-ledger-queries/application-javascript/.eslintignore b/asset-transfer-ledger-queries/application-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/asset-transfer-ledger-queries/application-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-ledger-queries/application-javascript/.eslintrc.js b/asset-transfer-ledger-queries/application-javascript/.eslintrc.js new file mode 100644 index 0000000..6fa636b --- /dev/null +++ b/asset-transfer-ledger-queries/application-javascript/.eslintrc.js @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +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', + 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'] + } +}; diff --git a/asset-transfer-ledger-queries/application-javascript/.gitignore b/asset-transfer-ledger-queries/application-javascript/.gitignore new file mode 100644 index 0000000..21b287f --- /dev/null +++ b/asset-transfer-ledger-queries/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-ledger-queries/application-javascript/app.js b/asset-transfer-ledger-queries/application-javascript/app.js new file mode 100644 index 0000000..8047f55 --- /dev/null +++ b/asset-transfer-ledger-queries/application-javascript/app.js @@ -0,0 +1,244 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const channelName = 'mychannel'; +const chaincodeName = 'ledger'; +const mspOrg1 = 'Org1MSP'; + +const walletPath = path.join(__dirname, 'wallet'); +const userId = 'appUser'; + +function prettyJSONString(inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); +} + +// pre-requisites: +// - fabric-sample two organization test-network setup with two peers, ordering service, +// and 2 certificate authorities, with the state database using couchdb +// ===> from directory /fabric-samples/test-network +// ./network.sh up createChannel -ca -s couchdb +// - Use any of the asset-transfer-ledger-queries chaincodes deployed on the channel "mychannel" +// with the chaincode name of "ledger". 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 ledger -ccp ../asset-transfer-ledger-queries/chaincode-javascript/ -ccl javascript +// - Be sure that node.js is installed +// ===> from directory /fabric-samples/asset-transfer-ledger-queries/application-javascript +// node -v +// - npm installed code dependencies +// ===> from directory /fabric-samples/asset-transfer-ledger-queries/application-javascript +// npm install +// - to run this test application +// ===> from directory /fabric-samples/asset-transfer-ledger-queries/application-javascript +// node app.js + +// 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-ledger-queries/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. +// + +/** + * A test application to show ledger queries operations with any of the asset-transfer-ledger-queries 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() { + let skipInit = false; + if (process.argv.length > 2) { + if (process.argv[2] === 'skipInit') { + skipInit = true; + } + } + + 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(FabricCAServices, ccp, 'ca.org1.example.com'); + + // setup the wallet to hold the credentials of the application user + const wallet = await buildWallet(Wallets, 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, userId, '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(); + + 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, { + wallet, + identity: userId, + discovery: { enabled: true, asLocalhost: true } // using asLocalhost as this gateway is using a fabric network deployed locally + }); + + // 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. + if (!skipInit) { + try { + console.log('\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger'); + await contract.submitTransaction('InitLedger'); + console.log('*** Result: committed'); + } catch (initError) { + // this is error is OK if we are rerunning this app without restarting + console.log(`******** initLedger failed :: ${initError}`) + } + } else { + console.log('*** not executing "InitLedger'); + } + + let result; + + // Let's try a query operation (function). + // This will be sent to just one peer and the results will be shown. + console.log('\n--> Evaluate Transaction: GetAssetsByRange, function returns assets in a specific range from asset1 to before asset6'); + result = await contract.evaluateTransaction('GetAssetsByRange', 'asset1', 'asset6'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + console.log('\n--> Evaluate Transaction: GetAssetsByRange, function use an open start and open end range to return assest1 to asset6'); + result = await contract.evaluateTransaction('GetAssetsByRange', '', ''); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + console.log('\n--> Evaluate Transaction: GetAssetsByRange, function use an fixed start (asset3) and open end range to return assest3 to asset6'); + result = await contract.evaluateTransaction('GetAssetsByRange', 'asset3', ''); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + console.log('\n--> Evaluate Transaction: GetAssetsByRange, function use an open start and fixed end (asset3) range to return assest1 to asset2'); + result = await contract.evaluateTransaction('GetAssetsByRange', '', 'asset3'); + 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(asset7), color(yellow), size(5), owner(Tom), and appraisedValue(1300) arguments'); + await contract.submitTransaction('CreateAsset', 'asset7', 'yellow', '5', 'Tom', '1300'); + console.log('*** Result: committed'); + + console.log('\n--> Evaluate Transaction: ReadAsset, function returns information about an asset with ID(asset7)'); + result = await contract.evaluateTransaction('ReadAsset', 'asset7'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + console.log('\n--> Evaluate Transaction: AssetExists, function returns "true" if an asset with ID(asset7) exist'); + result = await contract.evaluateTransaction('AssetExists', 'asset7'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + // Now let's try to submit a transaction that deletes an asset + // 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: DeleteAsset with ID(asset7)'); + await contract.submitTransaction('DeleteAsset', 'asset7'); + console.log('*** Result: committed'); + + console.log('\n--> Evaluate Transaction: AssetExists, function returns "false" if an asset with ID(asset7) does not exist'); + result = await contract.evaluateTransaction('AssetExists', 'asset7'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`) + + console.log('\n--> Submit Transaction: TransferAsset, transfer asset(asset2) to new owner(Tom)'); + await contract.submitTransaction('TransferAsset', 'asset2', 'Tom'); + console.log('*** Result: committed'); + + console.log('\n--> Evaluate Transaction: ReadAsset, function returns information about an asset with ID(asset2)'); + result = await contract.evaluateTransaction('ReadAsset', 'asset2'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + // Rich Query with Pagination (Only supported if CouchDB is used as state database) + console.log('\n--> Evaluate Transaction: QueryAssetsWithPagination, function returns "Tom" assets'); + result = await contract.evaluateTransaction('QueryAssetsWithPagination', '{"selector":{"docType":"asset","owner":"Tom"}, "use_index":["_design/indexOwnerDoc", "indexOwner"]}', '3', ''); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + console.log('\n--> Submit Transaction: TransferAssetByColor, transfer all yellow assets to new owner(Michel)'); + await contract.submitTransaction('TransferAssetByColor', 'yellow', 'Michel'); + console.log('*** Result: committed'); + + // Rich Query (Only supported if CouchDB is used as state database): + console.log('\n--> Evaluate Transaction: QueryAssetsByOwner, find all assets with owner(Michel)'); + result = await contract.evaluateTransaction('QueryAssetsByOwner', 'Michel'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + console.log('\n--> Evaluate Transaction: GetAssetHistory, get the history of an asset(asset7)'); + result = await contract.evaluateTransaction('GetAssetHistory', 'asset7'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + // Rich Query (Only supported if CouchDB is used as state database): + console.log('\n--> Evaluate Transaction: QueryAssets, assets of size 15'); + result = await contract.evaluateTransaction('QueryAssets', '{"selector":{"size":15}}'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + // Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database): + console.log('\n--> Evaluate Transaction: QueryAssets, Jin Soo\'s assets'); + result = await contract.evaluateTransaction('QueryAssets', '{"selector":{"docType":"asset","owner":"Jin Soo"}, "use_index":["_design/indexOwnerDoc", "indexOwner"]}'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + // Rich Query with Pagination (Only supported if CouchDB is used as state database) + console.log('\n--> Evaluate Transaction: GetAssetsByRangeWithPagination - get page 1 of assets from asset3 to asset6 (asset3, asset4)'); + result = await contract.evaluateTransaction('GetAssetsByRangeWithPagination', 'asset3', 'asset6', '2', ''); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + // Rich Query with Pagination (Only supported if CouchDB is used as state database) + console.log('\n--> Evaluate Transaction: GetAssetsByRangeWithPagination - get page 2 of assets from asset3 to asset6 (asset4, asset5)'); + result = await contract.evaluateTransaction('GetAssetsByRangeWithPagination', 'asset3', 'asset6', '2', 'asset4'); + console.log(`*** Result: ${prettyJSONString(result.toString())}`); + + console.log('*** all tests completed'); + } 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}`); + } + + console.log('*** application ending'); + +} + +main(); diff --git a/asset-transfer-ledger-queries/application-javascript/package.json b/asset-transfer-ledger-queries/application-javascript/package.json new file mode 100644 index 0000000..f06a831 --- /dev/null +++ b/asset-transfer-ledger-queries/application-javascript/package.json @@ -0,0 +1,16 @@ +{ + "name": "asset-transfer-ledger-queries", + "version": "1.0.0", + "description": "Asset transfer ledger queries application implemented in JavaScript", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4" + } +} diff --git a/asset-transfer-ledger-queries/chaincode-go/META-INF/statedb/couchdb/indexes/indexOwner.json b/asset-transfer-ledger-queries/chaincode-go/META-INF/statedb/couchdb/indexes/indexOwner.json new file mode 100644 index 0000000..305f090 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-go/META-INF/statedb/couchdb/indexes/indexOwner.json @@ -0,0 +1 @@ +{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} diff --git a/asset-transfer-ledger-queries/chaincode-go/asset_transfer_ledger_chaincode.go b/asset-transfer-ledger-queries/chaincode-go/asset_transfer_ledger_chaincode.go new file mode 100644 index 0000000..44cbdb1 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-go/asset_transfer_ledger_chaincode.go @@ -0,0 +1,466 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ + +/* +====CHAINCODE EXECUTION SAMPLES (CLI) ================== + +==== Invoke assets ==== +peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset1","blue","5","tom","35"]}' +peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset2","red","4","tom","50"]}' +peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset3","blue","6","tom","70"]}' +peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["TransferAsset","asset2","jerry"]}' +peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["TransferAssetByColor","blue","jerry"]}' +peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["DeleteAsset","asset1"]}' + +==== Query assets ==== +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["ReadAsset","asset1"]}' +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["GetAssetsByRange","asset1","asset3"]}' +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["GetAssetHistory","asset1"]}' + +Rich Query (Only supported if CouchDB is used as state database): +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssetsByOwner","tom"]}' +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"owner\":\"tom\"}}"]}' + +Rich Query with Pagination (Only supported if CouchDB is used as state database): +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssetsWithPagination","{\"selector\":{\"owner\":\"tom\"}}","3",""]}' + +INDEXES TO SUPPORT COUCHDB RICH QUERIES + +Indexes in CouchDB are required in order to make JSON queries efficient and are required for +any JSON query with a sort. Indexes may be packaged alongside +chaincode in a META-INF/statedb/couchdb/indexes directory. Each index must be defined in its own +text file with extension *.json with the index definition formatted in JSON following the +CouchDB index JSON syntax as documented at: +http://docs.couchdb.org/en/2.3.1/api/database/find.html#db-index + +This asset transfer ledger example chaincode demonstrates a packaged +index which you can find in META-INF/statedb/couchdb/indexes/indexOwner.json. + +If you have access to the your peer's CouchDB state database in a development environment, +you may want to iteratively test various indexes in support of your chaincode queries. You +can use the CouchDB Fauxton interface or a command line curl utility to create and update +indexes. Then once you finalize an index, include the index definition alongside your +chaincode in the META-INF/statedb/couchdb/indexes directory, for packaging and deployment +to managed environments. + +In the examples below you can find index definitions that support asset transfer ledger +chaincode queries, along with the syntax that you can use in development environments +to create the indexes in the CouchDB Fauxton interface or a curl command line utility. + + +Index for docType, owner. + +Example curl command line to define index in the CouchDB channel_chaincode database +curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[\"docType\",\"owner\"]},\"name\":\"indexOwner\",\"ddoc\":\"indexOwnerDoc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index + + +Index for docType, owner, size (descending order). + +Example curl command line to define index in the CouchDB channel_chaincode database: +curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[{\"size\":\"desc\"},{\"docType\":\"desc\"},{\"owner\":\"desc\"}]},\"ddoc\":\"indexSizeSortDoc\", \"name\":\"indexSizeSortDesc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index + +Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database): +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":\"asset\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}' + +Rich Query with index design doc specified only (Only supported if CouchDB is used as state database): +peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":{\"$eq\":\"asset\"},\"owner\":{\"$eq\":\"tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}' +*/ + +package main + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +const index = "color~name" + +// SimpleChaincode implements the fabric-contract-api-go programming model +type SimpleChaincode struct { + contractapi.Contract +} + +type Asset struct { + DocType string `json:"docType"` //docType is used to distinguish the various types of objects in state database + ID string `json:"ID"` //the field tags are needed to keep case from bouncing around + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` + AppraisedValue int `json:"appraisedValue"` +} + +// HistoryQueryResult structure used for returning result of history query +type HistoryQueryResult struct { + Record *Asset `json:"record"` + TxId string `json:"txId"` + Timestamp time.Time `json:"timestamp"` + IsDelete bool `json:"isDelete"` +} + +// PaginatedQueryResult structure used for returning paginated query results and metadata +type PaginatedQueryResult struct { + Records []*Asset `json:"records"` + FetchedRecordsCount int32 `json:"fetchedRecordsCount"` + Bookmark string `json:"bookmark"` +} + +// CreateAsset initializes a new asset in the ledger +func (t *SimpleChaincode) CreateAsset(ctx contractapi.TransactionContextInterface, assetID, color string, size int, owner string, appraisedValue int) error { + exists, err := t.AssetExists(ctx, assetID) + if err != nil { + return fmt.Errorf("failed to get asset: %v", err) + } + if exists { + return fmt.Errorf("asset already exists: %s", assetID) + } + + asset := &Asset{ + DocType: "asset", + ID: assetID, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + } + assetBytes, err := json.Marshal(asset) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(assetID, assetBytes) + if err != nil { + return err + } + + // Create an index to enable color-based range queries, e.g. return all blue assets. + // An 'index' is a normal key-value entry in the ledger. + // The key is a composite key, with the elements that you want to range query on listed first. + // In our case, the composite key is based on indexName~color~name. + // This will enable very efficient state range queries based on composite keys matching indexName~color~* + colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(index, []string{asset.Color, asset.ID}) + if err != nil { + return err + } + // Save index entry to world state. Only the key name is needed, no need to store a duplicate copy of the asset. + // Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value + value := []byte{0x00} + return ctx.GetStub().PutState(colorNameIndexKey, value) +} + +// ReadAsset retrieves an asset from the ledger +func (t *SimpleChaincode) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) { + assetBytes, err := ctx.GetStub().GetState(assetID) + if err != nil { + return nil, fmt.Errorf("failed to get asset %s: %v", assetID, err) + } + if assetBytes == nil { + return nil, fmt.Errorf("asset %s does not exist", assetID) + } + + var asset Asset + err = json.Unmarshal(assetBytes, &asset) + if err != nil { + return nil, err + } + + return &asset, nil +} + +// DeleteAsset removes an asset key-value pair from the ledger +func (t *SimpleChaincode) DeleteAsset(ctx contractapi.TransactionContextInterface, assetID string) error { + asset, err := t.ReadAsset(ctx, assetID) + if err != nil { + return err + } + + err = ctx.GetStub().DelState(assetID) + if err != nil { + return fmt.Errorf("failed to delete asset %s: %v", assetID, err) + } + + colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(index, []string{asset.Color, asset.ID}) + if err != nil { + return err + } + + // Delete index entry + return ctx.GetStub().DelState(colorNameIndexKey) +} + +// TransferAsset transfers an asset by setting a new owner name on the asset +func (t *SimpleChaincode) TransferAsset(ctx contractapi.TransactionContextInterface, assetID, newOwner string) error { + asset, err := t.ReadAsset(ctx, assetID) + if err != nil { + return err + } + + asset.Owner = newOwner + assetBytes, err := json.Marshal(asset) + if err != nil { + return err + } + + return ctx.GetStub().PutState(assetID, assetBytes) +} + +// constructQueryResponseFromIterator constructs a slice of assets from the resultsIterator +func constructQueryResponseFromIterator(resultsIterator shim.StateQueryIteratorInterface) ([]*Asset, error) { + var assets []*Asset + for resultsIterator.HasNext() { + queryResult, err := resultsIterator.Next() + if err != nil { + return nil, err + } + var asset Asset + err = json.Unmarshal(queryResult.Value, &asset) + if err != nil { + return nil, err + } + assets = append(assets, &asset) + } + + return assets, nil +} + +// GetAssetsByRange performs a range query based on the start and end keys provided. +// Read-only function results are not typically submitted to ordering. If the read-only +// results are submitted to ordering, or if the query is used in an update transaction +// and submitted to ordering, then the committing peers will re-execute to guarantee that +// result sets are stable between endorsement time and commit time. The transaction is +// invalidated by the committing peers if the result set has changed between endorsement +// time and commit time. +// Therefore, range queries are a safe option for performing update transactions based on query results. +func (t *SimpleChaincode) GetAssetsByRange(ctx contractapi.TransactionContextInterface, startKey, endKey string) ([]*Asset, error) { + resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + return constructQueryResponseFromIterator(resultsIterator) +} + +// TransferAssetByColor will transfer assets of a given color to a certain new owner. +// Uses GetStateByPartialCompositeKey (range query) against color~name 'index'. +// Committing peers will re-execute range queries to guarantee that result sets are stable +// between endorsement time and commit time. The transaction is invalidated by the +// committing peers if the result set has changed between endorsement time and commit time. +// Therefore, range queries are a safe option for performing update transactions based on query results. +// Example: GetStateByPartialCompositeKey/RangeQuery +func (t *SimpleChaincode) TransferAssetByColor(ctx contractapi.TransactionContextInterface, color, newOwner string) error { + // Execute a key range query on all keys starting with 'color' + coloredAssetResultsIterator, err := ctx.GetStub().GetStateByPartialCompositeKey(index, []string{color}) + if err != nil { + return err + } + defer coloredAssetResultsIterator.Close() + + for coloredAssetResultsIterator.HasNext() { + responseRange, err := coloredAssetResultsIterator.Next() + if err != nil { + return err + } + + _, compositeKeyParts, err := ctx.GetStub().SplitCompositeKey(responseRange.Key) + if err != nil { + return err + } + + if len(compositeKeyParts) > 1 { + returnedAssetID := compositeKeyParts[1] + asset, err := t.ReadAsset(ctx, returnedAssetID) + if err != nil { + return err + } + asset.Owner = newOwner + assetBytes, err := json.Marshal(asset) + if err != nil { + return err + } + err = ctx.GetStub().PutState(returnedAssetID, assetBytes) + if err != nil { + return fmt.Errorf("transfer failed for asset %s: %v", returnedAssetID, err) + } + } + } + + return nil +} + +// QueryAssetsByOwner queries for assets based on the owners name. +// This is an example of a parameterized query where the query logic is baked into the chaincode, +// and accepting a single query parameter (owner). +// Only available on state databases that support rich query (e.g. CouchDB) +// Example: Parameterized rich query +func (t *SimpleChaincode) QueryAssetsByOwner(ctx contractapi.TransactionContextInterface, owner string) ([]*Asset, error) { + queryString := fmt.Sprintf(`{"selector":{"docType":"asset","owner":"%s"}}`, owner) + return getQueryResultForQueryString(ctx, queryString) +} + +// QueryAssets uses a query string to perform a query for assets. +// Query string matching state database syntax is passed in and executed as is. +// Supports ad hoc queries that can be defined at runtime by the client. +// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries. +// Only available on state databases that support rich query (e.g. CouchDB) +// Example: Ad hoc rich query +func (t *SimpleChaincode) QueryAssets(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) { + return getQueryResultForQueryString(ctx, queryString) +} + +// getQueryResultForQueryString executes the passed in query string. +// The result set is built and returned as a byte array containing the JSON results. +func getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) { + resultsIterator, err := ctx.GetStub().GetQueryResult(queryString) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + return constructQueryResponseFromIterator(resultsIterator) +} + +// GetAssetsByRangeWithPagination performs a range query based on the start and end key, +// page size and a bookmark. +// The number of fetched records will be equal to or lesser than the page size. +// Paginated range queries are only valid for read only transactions. +// Example: Pagination with Range Query +func (t *SimpleChaincode) GetAssetsByRangeWithPagination(ctx contractapi.TransactionContextInterface, startKey string, endKey string, pageSize int, bookmark string) ([]*Asset, error) { + + resultsIterator, _, err := ctx.GetStub().GetStateByRangeWithPagination(startKey, endKey, int32(pageSize), bookmark) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + return constructQueryResponseFromIterator(resultsIterator) +} + +// QueryAssetsWithPagination uses a query string, page size and a bookmark to perform a query +// for assets. Query string matching state database syntax is passed in and executed as is. +// The number of fetched records would be equal to or lesser than the specified page size. +// Supports ad hoc queries that can be defined at runtime by the client. +// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries. +// Only available on state databases that support rich query (e.g. CouchDB) +// Paginated queries are only valid for read only transactions. +// Example: Pagination with Ad hoc Rich Query +func (t *SimpleChaincode) QueryAssetsWithPagination(ctx contractapi.TransactionContextInterface, queryString string, pageSize int, bookmark string) (*PaginatedQueryResult, error) { + + return getQueryResultForQueryStringWithPagination(ctx, queryString, int32(pageSize), bookmark) +} + +// getQueryResultForQueryStringWithPagination executes the passed in query string with +// pagination info. The result set is built and returned as a byte array containing the JSON results. +func getQueryResultForQueryStringWithPagination(ctx contractapi.TransactionContextInterface, queryString string, pageSize int32, bookmark string) (*PaginatedQueryResult, error) { + + resultsIterator, responseMetadata, err := ctx.GetStub().GetQueryResultWithPagination(queryString, pageSize, bookmark) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + assets, err := constructQueryResponseFromIterator(resultsIterator) + if err != nil { + return nil, err + } + + return &PaginatedQueryResult{ + Records: assets, + FetchedRecordsCount: responseMetadata.FetchedRecordsCount, + Bookmark: responseMetadata.Bookmark, + }, nil +} + +// GetAssetHistory returns the chain of custody for an asset since issuance. +func (t *SimpleChaincode) GetAssetHistory(ctx contractapi.TransactionContextInterface, assetID string) ([]HistoryQueryResult, error) { + log.Printf("GetAssetHistory: ID %v", assetID) + + resultsIterator, err := ctx.GetStub().GetHistoryForKey(assetID) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + var records []HistoryQueryResult + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + var asset Asset + if len(response.Value) > 0 { + err = json.Unmarshal(response.Value, &asset) + if err != nil { + return nil, err + } + } else { + asset = Asset{ + ID: assetID, + } + } + + timestamp, err := ptypes.Timestamp(response.Timestamp) + if err != nil { + return nil, err + } + + record := HistoryQueryResult{ + TxId: response.TxId, + Timestamp: timestamp, + Record: &asset, + IsDelete: response.IsDelete, + } + records = append(records, record) + } + + return records, nil +} + +// AssetExists returns true when asset with given ID exists in the ledger. +func (t *SimpleChaincode) AssetExists(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) { + assetBytes, err := ctx.GetStub().GetState(assetID) + if err != nil { + return false, fmt.Errorf("failed to read asset %s from world state. %v", assetID, err) + } + + return assetBytes != nil, nil +} + +// InitLedger creates the initial set of assets in the ledger. +func (t *SimpleChaincode) InitLedger(ctx contractapi.TransactionContextInterface) error { + assets := []Asset{ + {DocType: "asset", ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300}, + {DocType: "asset", ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400}, + {DocType: "asset", ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500}, + {DocType: "asset", ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600}, + {DocType: "asset", ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700}, + {DocType: "asset", ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800}, + } + + for _, asset := range assets { + err := t.CreateAsset(ctx, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue) + if err != nil { + return err + } + } + + return nil +} + +func main() { + chaincode, err := contractapi.NewChaincode(&SimpleChaincode{}) + if err != nil { + log.Panicf("Error creating asset chaincode: %v", err) + } + + if err := chaincode.Start(); err != nil { + log.Panicf("Error starting asset chaincode: %v", err) + } +} diff --git a/asset-transfer-ledger-queries/chaincode-go/go.mod b/asset-transfer-ledger-queries/chaincode-go/go.mod new file mode 100644 index 0000000..fc82521 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-go/go.mod @@ -0,0 +1,9 @@ +module github.com/hyperledger/fabric-samples/asset-transfer-ledger-queries/chaincode-go + +go 1.14 + +require ( + github.com/golang/protobuf v1.3.2 + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200511190512-bcfeb58dd83a + github.com/hyperledger/fabric-contract-api-go v1.1.0 +) diff --git a/asset-transfer-ledger-queries/chaincode-go/go.sum b/asset-transfer-ledger-queries/chaincode-go/go.sum new file mode 100644 index 0000000..1c4ebdf --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-go/go.sum @@ -0,0 +1,146 @@ +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/go.mod h1:N7H3sA7Tx4k/YzFq7U0EPdqJtqvM4Kild0JoCc7C0Dc= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200511190512-bcfeb58dd83a h1:KoFw2HnRfW+EItMP0zvUUl1FGzDb/7O0ov7uXZffQok= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200511190512-bcfeb58dd83a/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/asset-transfer-ledger-queries/chaincode-javascript/.eslintignore b/asset-transfer-ledger-queries/chaincode-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-ledger-queries/chaincode-javascript/.eslintrc.js b/asset-transfer-ledger-queries/chaincode-javascript/.eslintrc.js new file mode 100644 index 0000000..072edaf --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/.eslintrc.js @@ -0,0 +1,37 @@ +/* + * 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', + 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'] + } +}; diff --git a/asset-transfer-ledger-queries/chaincode-javascript/index.js b/asset-transfer-ledger-queries/chaincode-javascript/index.js new file mode 100644 index 0000000..5c7b6e0 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/index.js @@ -0,0 +1,13 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const CC = require('./lib/asset_transfer_ledger_chaincode.js'); + +module.exports.CC = CC; +module.exports.contracts = [ CC ]; diff --git a/asset-transfer-ledger-queries/chaincode-javascript/lib/META-INF/statedb/couchdb/indexes/indexOwner.json b/asset-transfer-ledger-queries/chaincode-javascript/lib/META-INF/statedb/couchdb/indexes/indexOwner.json new file mode 100644 index 0000000..305f090 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/lib/META-INF/statedb/couchdb/indexes/indexOwner.json @@ -0,0 +1 @@ +{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} 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 new file mode 100644 index 0000000..31174f4 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/lib/asset_transfer_ledger_chaincode.js @@ -0,0 +1,402 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ + +// ====CHAINCODE EXECUTION SAMPLES (CLI) ================== + +// ==== Invoke assets ==== +// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset1","blue","35","Tom","100"]}' +// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset2","red","50","Tom","150"]}' +// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset3","blue","70","Tom","200"]}' +// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["TransferAsset","asset2","jerry"]}' +// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["TransferAssetsBasedOnColor","blue","jerry"]}' +// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["DeleteAsset","asset1"]}' + +// ==== Query assets ==== +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["ReadAsset","asset1"]}' +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["GetAssetsByRange","asset1","asset3"]}' +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["GetAssetHistory","asset1"]}' + +// Rich Query (Only supported if CouchDB is used as state database): +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssetsByOwner","Tom"]}' output issue +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"owner\":\"Tom\"}}"]}' + +// Rich Query with Pagination (Only supported if CouchDB is used as state database): +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssetsWithPagination","{\"selector\":{\"owner\":\"Tom\"}}","3",""]}' + +// INDEXES TO SUPPORT COUCHDB RICH QUERIES +// +// Indexes in CouchDB are required in order to make JSON queries efficient and are required for +// any JSON query with a sort. Indexes may be packaged alongside +// chaincode in a META-INF/statedb/couchdb/indexes directory. Each index must be defined in its own +// text file with extension *.json with the index definition formatted in JSON following the +// CouchDB index JSON syntax as documented at: +// http://docs.couchdb.org/en/2.3.1/api/database/find.html#db-index +// +// This asset transfer ledger example chaincode demonstrates a packaged +// index which you can find in META-INF/statedb/couchdb/indexes/indexOwner.json. +// +// If you have access to the your peer's CouchDB state database in a development environment, +// you may want to iteratively test various indexes in support of your chaincode queries. You +// can use the CouchDB Fauxton interface or a command line curl utility to create and update +// indexes. Then once you finalize an index, include the index definition alongside your +// chaincode in the META-INF/statedb/couchdb/indexes directory, for packaging and deployment +// to managed environments. +// +// In the examples below you can find index definitions that support asset transfer ledger +// chaincode queries, along with the syntax that you can use in development environments +// to create the indexes in the CouchDB Fauxton interface or a curl command line utility. +// + +// Index for docType, owner. +// +// Example curl command line to define index in the CouchDB channel_chaincode database +// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[\"docType\",\"owner\"]},\"name\":\"indexOwner\",\"ddoc\":\"indexOwnerDoc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index +// + +// Index for docType, owner, size (descending order). +// +// Example curl command line to define index in the CouchDB channel_chaincode database +// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[{\"size\":\"desc\"},{\"docType\":\"desc\"},{\"owner\":\"desc\"}]},\"ddoc\":\"indexSizeSortDoc\", \"name\":\"indexSizeSortDesc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index + +// Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database): +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":\"asset\",\"owner\":\"Tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}' + +// Rich Query with index design doc specified only (Only supported if CouchDB is used as state database): +// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":{\"$eq\":\"asset\"},\"owner\":{\"$eq\":\"Tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}' + +'use strict'; + +const {Contract} = require('fabric-contract-api'); + +class Chaincode extends Contract { + + // CreateAsset - create a new asset, store into chaincode state + async CreateAsset(ctx, assetID, color, size, owner, appraisedValue) { + const exists = await this.AssetExists(ctx, assetID); + if (exists) { + throw new Error(`The asset ${assetID} already exists`); + } + + // ==== Create asset object and marshal to JSON ==== + let asset = { + docType: 'asset', + assetID: assetID, + color: color, + size: size, + owner: owner, + appraisedValue: appraisedValue + }; + + + // === Save asset to state === + await ctx.stub.putState(assetID, Buffer.from(JSON.stringify(asset))); + let indexName = 'color~name'; + let colorNameIndexKey = await ctx.stub.createCompositeKey(indexName, [asset.color, asset.assetID]); + + // Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble. + // Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value + await ctx.stub.putState(colorNameIndexKey, Buffer.from('\u0000')); + } + + // ReadAsset returns the asset stored in the world state with given id. + async ReadAsset(ctx, id) { + const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state + if (!assetJSON || assetJSON.length === 0) { + throw new Error(`Asset ${id} does not exist`); + } + + return assetJSON.toString(); + } + + // delete - remove a asset key/value pair from state + async DeleteAsset(ctx, id) { + if (!id) { + throw new Error('Asset name must not be empty'); + } + + let exists = await this.AssetExists(ctx, id); + if (!exists) { + throw new Error(`Asset ${id} does not exist`); + } + + // to maintain the color~name index, we need to read the asset first and get its color + let valAsbytes = await ctx.stub.getState(id); // get the asset from chaincode state + let jsonResp = {}; + if (!valAsbytes) { + jsonResp.error = `Asset does not exist: ${id}`; + throw new Error(jsonResp); + } + let assetJSON; + try { + assetJSON = JSON.parse(valAsbytes.toString()); + } catch (err) { + jsonResp = {}; + jsonResp.error = `Failed to decode JSON of: ${id}`; + throw new Error(jsonResp); + } + await ctx.stub.deleteState(id); //remove the asset from chaincode state + + // delete the index + let indexName = 'color~name'; + let colorNameIndexKey = ctx.stub.createCompositeKey(indexName, [assetJSON.color, assetJSON.assetID]); + if (!colorNameIndexKey) { + throw new Error(' Failed to create the createCompositeKey'); + } + // Delete index entry to state. + await ctx.stub.deleteState(colorNameIndexKey); + } + + // TransferAsset transfers a asset by setting a new owner name on the asset + async TransferAsset(ctx, assetName, newOwner) { + + let assetAsBytes = await ctx.stub.getState(assetName); + if (!assetAsBytes || !assetAsBytes.toString()) { + throw new Error(`Asset ${assetName} does not exist`); + } + let assetToTransfer = {}; + try { + assetToTransfer = JSON.parse(assetAsBytes.toString()); //unmarshal + } catch (err) { + let jsonResp = {}; + jsonResp.error = 'Failed to decode JSON of: ' + assetName; + throw new Error(jsonResp); + } + assetToTransfer.owner = newOwner; //change the owner + + let assetJSONasBytes = Buffer.from(JSON.stringify(assetToTransfer)); + await ctx.stub.putState(assetName, assetJSONasBytes); //rewrite the asset + } + + // GetAssetsByRange performs a range query based on the start and end keys provided. + // Read-only function results are not typically submitted to ordering. If the read-only + // results are submitted to ordering, or if the query is used in an update transaction + // and submitted to ordering, then the committing peers will re-execute to guarantee that + // result sets are stable between endorsement time and commit time. The transaction is + // invalidated by the committing peers if the result set has changed between endorsement + // time and commit time. + // Therefore, range queries are a safe option for performing update transactions based on query results. + async GetAssetsByRange(ctx, startKey, endKey) { + + let resultsIterator = await ctx.stub.getStateByRange(startKey, endKey); + let results = await this.GetAllResults(resultsIterator, false); + + return JSON.stringify(results); + } + + // TransferAssetBasedOnColor will transfer assets of a given color to a certain new owner. + // Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'. + // Committing peers will re-execute range queries to guarantee that result sets are stable + // between endorsement time and commit time. The transaction is invalidated by the + // committing peers if the result set has changed between endorsement time and commit time. + // Therefore, range queries are a safe option for performing update transactions based on query results. + // Example: GetStateByPartialCompositeKey/RangeQuery + async TransferAssetByColor(ctx, color, newOwner) { + // Query the color~name index by color + // This will execute a key range query on all keys starting with 'color' + let coloredAssetResultsIterator = await ctx.stub.getStateByPartialCompositeKey('color~name', [color]); + + // Iterate through result set and for each asset found, transfer to newOwner + let responseRange = await coloredAssetResultsIterator.next(); + while (!responseRange.done) { + if (!responseRange || !responseRange.value || !responseRange.value.key) { + return; + } + + let objectType; + let attributes; + ( + {objectType, attributes} = await ctx.stub.splitCompositeKey(responseRange.value.key) + ); + + console.log(objectType); + let returnedAssetName = attributes[1]; + + // Now call the transfer function for the found asset. + // Re-use the same function that is used to transfer individual assets + await this.TransferAsset(ctx, returnedAssetName, newOwner); + responseRange = await coloredAssetResultsIterator.next(); + } + } + + // QueryAssetsByOwner queries for assets based on a passed in owner. + // This is an example of a parameterized query where the query logic is baked into the chaincode, + // and accepting a single query parameter (owner). + // Only available on state databases that support rich query (e.g. CouchDB) + // Example: Parameterized rich query + async QueryAssetsByOwner(ctx, owner) { + let queryString = {}; + queryString.selector = {}; + queryString.selector.docType = 'asset'; + queryString.selector.owner = owner; + return await this.GetQueryResultForQueryString(ctx, JSON.stringify(queryString)); //shim.success(queryResults); + } + + // Example: Ad hoc rich query + // QueryAssets uses a query string to perform a query for assets. + // Query string matching state database syntax is passed in and executed as is. + // Supports ad hoc queries that can be defined at runtime by the client. + // If this is not desired, follow the QueryAssetsForOwner example for parameterized queries. + // Only available on state databases that support rich query (e.g. CouchDB) + async QueryAssets(ctx, queryString) { + return await this.GetQueryResultForQueryString(ctx, queryString); + } + + // GetQueryResultForQueryString executes the passed in query string. + // Result set is built and returned as a byte array containing the JSON results. + async GetQueryResultForQueryString(ctx, queryString) { + + let resultsIterator = await ctx.stub.getQueryResult(queryString); + let results = await this.GetAllResults(resultsIterator, false); + + return JSON.stringify(results); + } + + // Example: Pagination with Range Query + // GetAssetsByRangeWithPagination performs a range query based on the start & end key, + // page size and a bookmark. + // The number of fetched records will be equal to or lesser than the page size. + // Paginated range queries are only valid for read only transactions. + async GetAssetsByRangeWithPagination(ctx, startKey, endKey, pageSize, bookmark) { + + const {iterator, metadata} = await ctx.stub.getStateByRangeWithPagination(startKey, endKey, pageSize, bookmark); + const results = await this.GetAllResults(iterator, false); + + results.ResponseMetadata = { + RecordsCount: metadata.fetched_records_count, + Bookmark: metadata.bookmark, + }; + return JSON.stringify(results); + } + + // Example: Pagination with Ad hoc Rich Query + // QueryAssetsWithPagination uses a query string, page size and a bookmark to perform a query + // for assets. Query string matching state database syntax is passed in and executed as is. + // The number of fetched records would be equal to or lesser than the specified page size. + // Supports ad hoc queries that can be defined at runtime by the client. + // If this is not desired, follow the QueryAssetsForOwner example for parameterized queries. + // Only available on state databases that support rich query (e.g. CouchDB) + // Paginated queries are only valid for read only transactions. + async QueryAssetsWithPagination(ctx, queryString, pageSize, bookmark) { + + const {iterator, metadata} = await ctx.stub.getQueryResultWithPagination(queryString, pageSize, bookmark); + const results = await this.GetAllResults(iterator, false); + + results.ResponseMetadata = { + RecordsCount: metadata.fetched_records_count, + Bookmark: metadata.bookmark, + }; + + return JSON.stringify(results); + } + + // GetAssetHistory returns the chain of custody for an asset since issuance. + async GetAssetHistory(ctx, assetName) { + + let resultsIterator = await ctx.stub.getHistoryForKey(assetName); + let results = await this.GetAllResults(resultsIterator, true); + + return JSON.stringify(results); + } + + // AssetExists returns true when asset with given ID exists in world state + async AssetExists(ctx, assetName) { + // ==== Check if asset already exists ==== + let assetState = await ctx.stub.getState(assetName); + return assetState && assetState.length > 0; + } + + async GetAllResults(iterator, isHistory) { + let allResults = []; + let res = await iterator.next(); + while (!res.done) { + if (res.value && res.value.value.toString()) { + let jsonRes = {}; + console.log(res.value.value.toString('utf8')); + if (isHistory && isHistory === true) { + jsonRes.TxId = res.value.tx_id; + jsonRes.Timestamp = res.value.timestamp; + try { + jsonRes.Value = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Value = res.value.value.toString('utf8'); + } + } else { + jsonRes.Key = res.value.key; + try { + jsonRes.Record = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Record = res.value.value.toString('utf8'); + } + } + allResults.push(jsonRes); + } + res = await iterator.next(); + } + iterator.close(); + return allResults; + } + + // InitLedger creates sample assets in the ledger + async InitLedger(ctx) { + const assets = [ + { + assetID: 'asset1', + color: 'blue', + size: 5, + owner: 'Tom', + appraisedValue: 100 + }, + { + assetID: 'asset2', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 100 + }, + { + assetID: 'asset3', + color: 'green', + size: 10, + owner: 'Jin Soo', + appraisedValue: 200 + }, + { + assetID: 'asset4', + color: 'yellow', + size: 10, + owner: 'Max', + appraisedValue: 200 + }, + { + assetID: 'asset5', + color: 'black', + size: 15, + owner: 'Adriana', + appraisedValue: 250 + }, + { + assetID: 'asset6', + color: 'white', + size: 15, + owner: 'Michel', + appraisedValue: 250 + }, + ]; + + for (const asset of assets) { + await this.CreateAsset( + ctx, + asset.assetID, + asset.color, + asset.size, + asset.owner, + asset.appraisedValue + ); + } + } +} + +module.exports = Chaincode; diff --git a/asset-transfer-ledger-queries/chaincode-javascript/package.json b/asset-transfer-ledger-queries/chaincode-javascript/package.json new file mode 100644 index 0000000..0427b5c --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/package.json @@ -0,0 +1,19 @@ +{ + "name": "asset-transfer-ledger-queries", + "version": "1.0.0", + "description": "asset chaincode implemented in node.js", + "main": "index.js", + "engines": { + "node": ">=12", + "npm": ">=5.3.0" + }, + "scripts": { + "start": "fabric-chaincode-node start" + }, + "engine-strict": true, + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + } +} diff --git a/asset-transfer-private-data/application-javascript/.eslintignore b/asset-transfer-private-data/application-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/asset-transfer-private-data/application-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-private-data/application-javascript/.eslintrc.js b/asset-transfer-private-data/application-javascript/.eslintrc.js new file mode 100644 index 0000000..8b83df7 --- /dev/null +++ b/asset-transfer-private-data/application-javascript/.eslintrc.js @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + env: { + node: 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'] + } +}; diff --git a/asset-transfer-private-data/application-javascript/.gitignore b/asset-transfer-private-data/application-javascript/.gitignore new file mode 100644 index 0000000..b7db091 --- /dev/null +++ b/asset-transfer-private-data/application-javascript/.gitignore @@ -0,0 +1,16 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +wallet.org1 +wallet.org2 +!wallet.org1/.gitkeep +!wallet.org2/.gitkeep \ No newline at end of file diff --git a/asset-transfer-private-data/application-javascript/app.js b/asset-transfer-private-data/application-javascript/app.js new file mode 100644 index 0000000..77ac59f --- /dev/null +++ b/asset-transfer-private-data/application-javascript/app.js @@ -0,0 +1,358 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const myChannel = 'mychannel'; +const myChaincodeName = 'private'; + +const memberAssetCollectionName = 'assetCollection'; +const org1PrivateCollectionName = 'Org1MSPPrivateCollection'; +const org2PrivateCollectionName = 'Org2MSPPrivateCollection'; +const mspOrg1 = 'Org1MSP'; +const mspOrg2 = 'Org2MSP'; +const Org1UserId = 'appUser1'; +const Org2UserId = 'appUser2'; + +const RED = '\x1b[31m\n'; +const RESET = '\x1b[0m'; + +function prettyJSONString(inputString) { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} + +function doFail(msgString) { + console.error(`${RED}\t${msgString}${RESET}`); + process.exit(1); +} + +function verifyAssetData(org, resultBuffer, expectedId, color, size, ownerUserId, 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}`); + } + + if (asset.owner.includes(ownerUserId)) { + console.log(`\tasset ${asset.assetID} owner: ${asset.owner}`); + } else { + doFail(`Failed owner check from ${org} - asset ${asset.assetID} owned by ${asset.owner}, expected userId ${ownerUserId}`); + } + 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) + 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, mspOrg1); + // 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, mspOrg1, Org1UserId, 'org1.department1'); + + try { + // Create a new gateway for connecting to Org's peer node. + const gatewayOrg1 = new Gateway(); + //connect using Discovery enabled + await gatewayOrg1.connect(ccpOrg1, + { wallet: walletOrg1, identity: Org1UserId, discovery: { enabled: true, asLocalhost: true } }); + + return gatewayOrg1; + } catch (error) { + console.error(`Error in connecting to gateway: ${error}`); + process.exit(1); + } +} + +async function initContractFromOrg2Identity() { + console.log('\n--> Fabric client user & Gateway init: Using Org2 identity to Org2 Peer'); + 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); + await registerAndEnrollUser(caOrg2Client, walletOrg2, mspOrg2, Org2UserId, 'org2.department1'); + + try { + // Create a new gateway for connecting to Org's peer node. + const gatewayOrg2 = new Gateway(); + await gatewayOrg2.connect(ccpOrg2, + { wallet: walletOrg2, identity: Org2UserId, discovery: { enabled: true, asLocalhost: true } }); + + return gatewayOrg2; + } catch (error) { + console.error(`Error in connecting to gateway: ${error}`); + process.exit(1); + } +} + +// Main workflow : usecase details at asset-transfer-private-data/chaincode-go/README.md +// This app uses fabric-samples/test-network based setup and the companion chaincode +// For this usecase illustration, we will use both Org1 & Org2 client identity from this same app +// In real world the Org1 & Org2 identity will be used in different apps to achieve asset transfer. +async function main() { + try { + + /** ******* Fabric client init: Using Org1 identity to Org1 Peer ********** */ + const gatewayOrg1 = await initContractFromOrg1Identity(); + const networkOrg1 = await gatewayOrg1.getNetwork(myChannel); + const contractOrg1 = networkOrg1.getContract(myChaincodeName); + // Since this sample chaincode uses, Private Data Collection level endorsement policy, addDiscoveryInterest + // scopes the discovery service further to use the endorsement policies of collections, if any + contractOrg1.addDiscoveryInterest({ name: myChaincodeName, collectionNames: [memberAssetCollectionName, org1PrivateCollectionName] }); + + /** ~~~~~~~ Fabric client init: Using Org2 identity to Org2 Peer ~~~~~~~ */ + const gatewayOrg2 = await initContractFromOrg2Identity(); + const networkOrg2 = await gatewayOrg2.getNetwork(myChannel); + const contractOrg2 = networkOrg2.getContract(myChaincodeName); + contractOrg2.addDiscoveryInterest({ name: myChaincodeName, collectionNames: [memberAssetCollectionName, org2PrivateCollectionName] }); + try { + // Sample transactions are listed below + // Add few sample Assets & transfers one of the asset from Org1 to Org2 as the new owner + let 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 }; + let asset2Data = { objectType: assetType, assetID: assetID2, color: 'blue', size: 35, appraisedValue: 727 }; + + console.log('\n**************** As Org1 Client ****************'); + console.log('Adding Assets to work with:\n--> Submit Transaction: CreateAsset ' + assetID1); + let statefulTxn = contractOrg1.createTransaction('CreateAsset'); + //if you need to customize endorsement to specific set of Orgs, use setEndorsingOrganizations + //statefulTxn.setEndorsingOrganizations(mspOrg1); + let tmapData = Buffer.from(JSON.stringify(asset1Data)); + statefulTxn.setTransient({ + asset_properties: tmapData + }); + result = await statefulTxn.submit(); + + //Add asset2 + console.log('\n--> Submit Transaction: CreateAsset ' + assetID2); + statefulTxn = contractOrg1.createTransaction('CreateAsset'); + tmapData = Buffer.from(JSON.stringify(asset2Data)); + statefulTxn.setTransient({ + asset_properties: tmapData + }); + result = await statefulTxn.submit(); + + + console.log('\n--> 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())}`); + 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())}`); + verifyAssetPrivateDetails(result, assetID1, 100); + + // Attempt Transfer the asset to Org2 , without Org2 adding AgreeToTransfer // + // Transaction should return an error: "failed transfer verification ..." + let buyerDetails = { assetID: assetID1, buyerMSP: mspOrg2 }; + try { + console.log('\n--> Attempt Submit Transaction: TransferAsset ' + assetID1); + statefulTxn = contractOrg1.createTransaction('TransferAsset'); + tmapData = Buffer.from(JSON.stringify(buyerDetails)); + statefulTxn.setTransient({ + asset_owner: tmapData + }); + result = await statefulTxn.submit(); + console.log('******** FAILED: above operation expected to return an error'); + } catch (error) { + console.log(` Successfully caught the error: \n ${error}`); + } + 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())}`); + verifyAssetData(mspOrg2, result, assetID1, 'green', 20, Org1UserId); + + + // Org2 cannot ReadAssetPrivateDetails from Org1's private collection due to Collection policy + // Will fail: await contractOrg2.evaluateTransaction('ReadAssetPrivateDetails', org1PrivateCollectionName, assetID1); + + // Buyer from Org2 agrees to buy the asset assetID1 // + // To purchase the asset, the buyer needs to agree to the same value as the asset owner + let dataForAgreement = { assetID: assetID1, appraisedValue: 100 }; + console.log('\n--> Submit Transaction: AgreeToTransfer payload ' + JSON.stringify(dataForAgreement)); + statefulTxn = contractOrg2.createTransaction('AgreeToTransfer'); + tmapData = Buffer.from(JSON.stringify(dataForAgreement)); + statefulTxn.setTransient({ + asset_value: tmapData + }); + result = await statefulTxn.submit(); + + //Buyer can withdraw the Agreement, using DeleteTranferAgreement + /*statefulTxn = contractOrg2.createTransaction('DeleteTranferAgreement'); + statefulTxn.setEndorsingOrganizations(mspOrg2); + let dataForDeleteAgreement = { assetID: assetID1 }; + tmapData = Buffer.from(JSON.stringify(dataForDeleteAgreement)); + statefulTxn.setTransient({ + agreement_delete: tmapData + }); + result = await statefulTxn.submit();*/ + + console.log('\n**************** As Org1 Client ****************'); + // All members can send txn ReadTransferAgreement, set by Org2 above + console.log('\n--> Evaluate Transaction: ReadTransferAgreement ' + assetID1); + result = await contractOrg1.evaluateTransaction('ReadTransferAgreement', assetID1); + console.log(`<-- result: ${prettyJSONString(result.toString())}`); + + // Transfer the asset to Org2 // + // To transfer the asset, the owner needs to pass the MSP ID of new asset owner, and initiate the transfer + console.log('\n--> Submit Transaction: TransferAsset ' + assetID1); + + statefulTxn = contractOrg1.createTransaction('TransferAsset'); + tmapData = Buffer.from(JSON.stringify(buyerDetails)); + statefulTxn.setTransient({ + asset_owner: tmapData + }); + result = await statefulTxn.submit(); + + //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())}`); + verifyAssetData(mspOrg1, result, assetID1, 'green', 20, Org2UserId); + + //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())}`); + 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())}`); + verifyAssetData(mspOrg1, result, assetID2, 'blue', 35, Org1UserId); + + console.log('\n********* Demo deleting asset **************'); + let dataForDelete = { assetID: assetID2 }; + try { + //Non-owner Org2 should not be able to DeleteAsset. Expect an error from DeleteAsset + console.log('--> Attempt Transaction: as Org2 DeleteAsset ' + assetID2); + statefulTxn = contractOrg2.createTransaction('DeleteAsset'); + tmapData = Buffer.from(JSON.stringify(dataForDelete)); + statefulTxn.setTransient({ + asset_delete: tmapData + }); + result = await statefulTxn.submit(); + console.log('******** FAILED : expected to return an error'); + } catch (error) { + console.log(` Successfully caught the error: \n ${error}`); + } + // Delete Asset2 as Org1 + console.log('--> Submit Transaction: as Org1 DeleteAsset ' + assetID2); + statefulTxn = contractOrg1.createTransaction('DeleteAsset'); + tmapData = Buffer.from(JSON.stringify(dataForDelete)); + statefulTxn.setTransient({ + asset_delete: tmapData + }); + result = await statefulTxn.submit(); + + console.log('\n--> Evaluate Transaction: ReadAsset ' + assetID2); + result = await contractOrg1.evaluateTransaction('ReadAsset', assetID2); + 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())}`); + verifyAssetPrivateDetails(result, assetID1, 100); + } finally { + // Disconnect from the gateway peer when all work for this client identity is complete + gatewayOrg1.disconnect(); + gatewayOrg2.disconnect(); + } + } catch (error) { + console.error(`Error in transaction: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +} + +main(); diff --git a/asset-transfer-private-data/application-javascript/package.json b/asset-transfer-private-data/application-javascript/package.json new file mode 100644 index 0000000..86d8b24 --- /dev/null +++ b/asset-transfer-private-data/application-javascript/package.json @@ -0,0 +1,45 @@ +{ + "name": "asset-transfer-private-data", + "version": "1.0.0", + "description": "asset-transfer-private-data application implemented in JavaScript", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4" + }, + "devDependencies": { + "chai": "^4.2.0", + "eslint": "^5.9.0", + "mocha": "^5.2.0", + "nyc": "^14.1.1", + "sinon": "^7.1.1", + "sinon-chai": "^3.3.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} 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 new file mode 100644 index 0000000..e2d1d08 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json @@ -0,0 +1,12 @@ +{ + "index": { + "fields": [ + "objectType", + "owner" + ] + }, + "ddoc": "indexOwnerDoc", + "name": "indexOwner", + "type": "json" +} + diff --git a/asset-transfer-private-data/chaincode-go/README.md b/asset-transfer-private-data/chaincode-go/README.md new file mode 100644 index 0000000..f87a95c --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/README.md @@ -0,0 +1 @@ +[Using Private Data tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/private_data_tutorial.html) diff --git a/asset-transfer-private-data/chaincode-go/chaincode/asset_queries.go b/asset-transfer-private-data/chaincode-go/chaincode/asset_queries.go new file mode 100644 index 0000000..8672e31 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/asset_queries.go @@ -0,0 +1,190 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// ReadAsset reads the information from collection +func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) { + + log.Printf("ReadAsset: collection %v, ID %v", assetCollection, assetID) + assetJSON, err := ctx.GetStub().GetPrivateData(assetCollection, assetID) //get the asset from chaincode state + if err != nil { + return nil, fmt.Errorf("failed to read asset: %v", err) + } + + //No Asset found, return empty response + if assetJSON == nil { + log.Printf("%v does not exist in collection %v", assetID, assetCollection) + return nil, nil + } + + var asset *Asset + err = json.Unmarshal(assetJSON, &asset) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + return asset, nil + +} + +// ReadAssetPrivateDetails reads the asset private details in organization specific collection +func (s *SmartContract) ReadAssetPrivateDetails(ctx contractapi.TransactionContextInterface, collection string, assetID string) (*AssetPrivateDetails, error) { + log.Printf("ReadAssetPrivateDetails: collection %v, ID %v", collection, assetID) + assetDetailsJSON, err := ctx.GetStub().GetPrivateData(collection, assetID) // Get the asset from chaincode state + if err != nil { + return nil, fmt.Errorf("failed to read asset details: %v", err) + } + if assetDetailsJSON == nil { + log.Printf("AssetPrivateDetails for %v does not exist in collection %v", assetID, collection) + return nil, nil + } + + var assetDetails *AssetPrivateDetails + err = json.Unmarshal(assetDetailsJSON, &assetDetails) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + return assetDetails, nil +} + +// ReadTransferAgreement gets the buyer's identity from the transfer agreement from collection +func (s *SmartContract) ReadTransferAgreement(ctx contractapi.TransactionContextInterface, assetID string) (*TransferAgreement, error) { + log.Printf("ReadTransferAgreement: collection %v, ID %v", assetCollection, assetID) + // composite key for TransferAgreement of this asset + transferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{assetID}) + if err != nil { + return nil, fmt.Errorf("failed to create composite key: %v", err) + } + + buyerIdentity, err := ctx.GetStub().GetPrivateData(assetCollection, transferAgreeKey) // Get the identity from collection + if err != nil { + return nil, fmt.Errorf("failed to read TransferAgreement: %v", err) + } + if buyerIdentity == nil { + log.Printf("TransferAgreement for %v does not exist", assetID) + return nil, nil + } + agreement := &TransferAgreement{ + ID: assetID, + BuyerID: string(buyerIdentity), + } + return agreement, nil +} + +// GetAssetByRange performs a range query based on the start and end keys provided. Range +// queries can be used to read data from private data collections, but can not be used in +// a transaction that also writes to private data. +func (s *SmartContract) GetAssetByRange(ctx contractapi.TransactionContextInterface, startKey string, endKey string) ([]*Asset, error) { + + resultsIterator, err := ctx.GetStub().GetPrivateDataByRange(assetCollection, startKey, endKey) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []*Asset{} + + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + var asset *Asset + err = json.Unmarshal(response.Value, &asset) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + results = append(results, asset) + } + + return results, nil + +} + +// =======Rich queries ========================================================================= +// Two examples of rich queries are provided below (parameterized query and ad hoc query). +// Rich queries pass a query string to the state database. +// Rich queries are only supported by state database implementations +// that support rich query (e.g. CouchDB). +// The query string is in the syntax of the underlying state database. +// With rich queries there is no guarantee that the result set hasn't changed between +// endorsement time and commit time, aka 'phantom reads'. +// Therefore, rich queries should not be used in update transactions, unless the +// application handles the possibility of result set changes between endorsement and commit time. +// Rich queries can be used for point-in-time queries against a peer. +// ============================================================================================ + +// ===== Example: Parameterized rich query ================================================= + +// QueryAssetByOwner queries for assets based on assetType, owner. +// This is an example of a parameterized query where the query logic is baked into the chaincode, +// and accepting a single query parameter (owner). +// Only available on state databases that support rich query (e.g. CouchDB) +// ========================================================================================= +func (s *SmartContract) QueryAssetByOwner(ctx contractapi.TransactionContextInterface, assetType string, owner string) ([]*Asset, error) { + + queryString := fmt.Sprintf("{\"selector\":{\"objectType\":\"%v\",\"owner\":\"%v\"}}", assetType, owner) + + queryResults, err := s.getQueryResultForQueryString(ctx, queryString) + if err != nil { + return nil, err + } + return queryResults, nil +} + +// QueryAssets uses a query string to perform a query for assets. +// Query string matching state database syntax is passed in and executed as is. +// Supports ad hoc queries that can be defined at runtime by the client. +// If this is not desired, follow the QueryAssetByOwner example for parameterized queries. +// Only available on state databases that support rich query (e.g. CouchDB) +func (s *SmartContract) QueryAssets(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) { + + queryResults, err := s.getQueryResultForQueryString(ctx, queryString) + if err != nil { + return nil, err + } + return queryResults, nil +} + +// getQueryResultForQueryString executes the passed in query string. +func (s *SmartContract) getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) { + + resultsIterator, err := ctx.GetStub().GetPrivateDataQueryResult(assetCollection, queryString) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []*Asset{} + + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + var asset *Asset + + err = json.Unmarshal(response.Value, &asset) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + results = append(results, asset) + } + return results, nil +} diff --git a/asset-transfer-private-data/chaincode-go/chaincode/asset_queries_test.go b/asset-transfer-private-data/chaincode-go/chaincode/asset_queries_test.go new file mode 100644 index 0000000..3663c87 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/asset_queries_test.go @@ -0,0 +1,185 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +package chaincode_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/fabric-protos-go/ledger/queryresult" + + "github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go/chaincode" + "github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go/chaincode/mocks" + "github.com/stretchr/testify/require" +) + +/* +For details on generating the mocks, see comments in the file asset_transfer_test.go +*/ +func TestReadAsset(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + + assetBytes, err := assetTransferCC.ReadAsset(transactionContext, "id1") + require.NoError(t, err) + require.Nil(t, assetBytes) + + chaincodeStub.GetPrivateDataReturns(nil, fmt.Errorf("unable to retrieve asset")) + assetBytes, err = assetTransferCC.ReadAsset(transactionContext, "id1") + require.EqualError(t, err, "failed to read asset: unable to retrieve asset") + + testAsset := &chaincode.Asset{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + Owner: myOrg1Clientid, + } + setReturnPrivateDataInStub(t, chaincodeStub, testAsset) + assetRead, err := assetTransferCC.ReadAsset(transactionContext, "id1") + require.NoError(t, err) + require.Equal(t, testAsset, assetRead) +} + +func TestReadAssetPrivateDetails(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + + assetBytes, err := assetTransferCC.ReadAssetPrivateDetails(transactionContext, myOrg1PrivCollection, "id1") + require.NoError(t, err) + require.Nil(t, assetBytes) + + //read from the collection with no access + chaincodeStub.GetPrivateDataReturns(nil, fmt.Errorf("collection not found")) + assetBytes, err = assetTransferCC.ReadAssetPrivateDetails(transactionContext, myOrg2PrivCollection, "id1") + require.EqualError(t, err, "failed to read asset details: collection not found") + + returnPrivData := &chaincode.AssetPrivateDetails{ + ID: "id1", + AppraisedValue: 5, + } + setReturnAssetPrivateDetailsInStub(t, chaincodeStub, returnPrivData) + assetRead, err := assetTransferCC.ReadAssetPrivateDetails(transactionContext, myOrg1PrivCollection, "id1") + require.NoError(t, err) + require.Equal(t, returnPrivData, assetRead) +} + +func TestReadTransferAgreement(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + + //TransferAgreement does not exist + assetBytes, err := assetTransferCC.ReadTransferAgreement(transactionContext, "id1") + require.NoError(t, err) + require.Nil(t, assetBytes) + + chaincodeStub.GetPrivateDataReturns([]byte(myOrg2Clientid), nil) + expectedData := &chaincode.TransferAgreement{ + ID: "id1", + BuyerID: myOrg2Clientid, + } + dataRead, err := assetTransferCC.ReadTransferAgreement(transactionContext, "id1") + require.NoError(t, err) + require.Equal(t, expectedData, dataRead) +} + +func TestQueryAssetByOwner(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + + asset := &chaincode.Asset{Type: "valuableasset", ID: "asset1", Owner: "user1"} + asset1Bytes, err := json.Marshal(asset) + require.NoError(t, err) + + iterator := &mocks.StateQueryIterator{} + iterator.HasNextReturnsOnCall(0, true) + iterator.HasNextReturnsOnCall(1, false) + iterator.NextReturns(&queryresult.KV{Value: asset1Bytes}, nil) + chaincodeStub.GetPrivateDataQueryResultReturns(iterator, nil) + + assetTransferCC := &chaincode.SmartContract{} + assets, err := assetTransferCC.QueryAssetByOwner(transactionContext, "valuableasset", "user1") + require.NoError(t, err) + require.Equal(t, []*chaincode.Asset{asset}, assets) + + iterator.HasNextReturns(true) + iterator.NextReturns(nil, fmt.Errorf("failed retrieving next item")) + assets, err = assetTransferCC.QueryAssetByOwner(transactionContext, "valuableasset", "user1") + require.EqualError(t, err, "failed retrieving next item") + require.Nil(t, assets) + +} + +func TestQueryAssets(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + //Iterator with no records + iterator := &mocks.StateQueryIterator{} + iterator.HasNextReturns(false) + chaincodeStub.GetPrivateDataQueryResultReturns(iterator, nil) + + assetTransferCC := &chaincode.SmartContract{} + assets, err := assetTransferCC.QueryAssets(transactionContext, "querystr") + require.NoError(t, err) + require.Equal(t, []*chaincode.Asset{}, assets) + + iterator = &mocks.StateQueryIterator{} + chaincodeStub.GetPrivateDataQueryResultReturns(iterator, nil) + iterator.HasNextReturns(true) + iterator.NextReturns(nil, fmt.Errorf("failed retrieving next item")) + assets, err = assetTransferCC.QueryAssets(transactionContext, "querystr") + require.EqualError(t, err, "failed retrieving next item") + require.Nil(t, assets) + + asset := &chaincode.Asset{Type: "valuableasset", ID: "asset1", Owner: "user1"} + asset1Bytes, err := json.Marshal(asset) + require.NoError(t, err) + + iterator = &mocks.StateQueryIterator{} + chaincodeStub.GetPrivateDataQueryResultReturns(iterator, nil) + iterator.HasNextReturnsOnCall(0, true) + iterator.HasNextReturnsOnCall(1, false) + iterator.NextReturns(&queryresult.KV{Value: asset1Bytes}, nil) + + assets, err = assetTransferCC.QueryAssets(transactionContext, "querystr") + require.NoError(t, err) + require.Equal(t, []*chaincode.Asset{asset}, assets) +} + +func TestGetAssetByRange(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + //Iterator with no records + iterator := &mocks.StateQueryIterator{} + iterator.HasNextReturns(false) + chaincodeStub.GetPrivateDataByRangeReturns(iterator, nil) + + assetTransferCC := &chaincode.SmartContract{} + assets, err := assetTransferCC.GetAssetByRange(transactionContext, "st", "end") + require.NoError(t, err) + require.Equal(t, []*chaincode.Asset{}, assets) + + iterator = &mocks.StateQueryIterator{} + chaincodeStub.GetPrivateDataByRangeReturns(iterator, nil) + iterator.HasNextReturns(true) + iterator.NextReturns(nil, fmt.Errorf("failed retrieving next item")) + assets, err = assetTransferCC.GetAssetByRange(transactionContext, "st", "end") + require.EqualError(t, err, "failed retrieving next item") + require.Nil(t, assets) + + asset := &chaincode.Asset{Type: "valuableasset", ID: "asset1", Owner: "user1"} + asset1Bytes, err := json.Marshal(asset) + require.NoError(t, err) + + iterator = &mocks.StateQueryIterator{} + chaincodeStub.GetPrivateDataByRangeReturns(iterator, nil) + iterator.HasNextReturnsOnCall(0, true) + iterator.HasNextReturnsOnCall(1, false) + iterator.NextReturns(&queryresult.KV{Value: asset1Bytes}, nil) + + assets, err = assetTransferCC.GetAssetByRange(transactionContext, "st", "end") + require.NoError(t, err) + require.Equal(t, []*chaincode.Asset{asset}, assets) + +} diff --git a/asset-transfer-private-data/chaincode-go/chaincode/asset_transfer.go b/asset-transfer-private-data/chaincode-go/chaincode/asset_transfer.go new file mode 100644 index 0000000..59eae2a --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/asset_transfer.go @@ -0,0 +1,590 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "log" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +const assetCollection = "assetCollection" +const transferAgreementObjectType = "transferAgreement" + +// SmartContract of this fabric sample +type SmartContract struct { + contractapi.Contract +} + +// Asset describes main asset details that are visible to all organizations +type Asset struct { + Type string `json:"objectType"` //Type is used to distinguish the various types of objects in state database + ID string `json:"assetID"` + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` +} + +// AssetPrivateDetails describes details that are private to owners +type AssetPrivateDetails struct { + ID string `json:"assetID"` + AppraisedValue int `json:"appraisedValue"` +} + +// TransferAgreement describes the buyer agreement returned by ReadTransferAgreement +type TransferAgreement struct { + ID string `json:"assetID"` + BuyerID string `json:"buyerID"` +} + +// CreateAsset creates a new asset by placing the main asset details in the assetCollection +// that can be read by both organizations. The appraisal value is stored in the owners org specific collection. +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface) error { + + // Get new asset from transient map + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Asset properties are private, therefore they get passed in transient field, instead of func args + transientAssetJSON, ok := transientMap["asset_properties"] + if !ok { + //log error to stdout + return fmt.Errorf("asset not found in the transient map input") + } + + type assetTransientInput struct { + Type string `json:"objectType"` //Type is used to distinguish the various types of objects in state database + ID string `json:"assetID"` + Color string `json:"color"` + Size int `json:"size"` + AppraisedValue int `json:"appraisedValue"` + } + + var assetInput assetTransientInput + err = json.Unmarshal(transientAssetJSON, &assetInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetInput.Type) == 0 { + return fmt.Errorf("objectType field must be a non-empty string") + } + if len(assetInput.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + if len(assetInput.Color) == 0 { + return fmt.Errorf("color field must be a non-empty string") + } + if assetInput.Size <= 0 { + return fmt.Errorf("size field must be a positive integer") + } + if assetInput.AppraisedValue <= 0 { + return fmt.Errorf("appraisedValue field must be a positive integer") + } + + // Check if asset already exists + assetAsBytes, err := ctx.GetStub().GetPrivateData(assetCollection, assetInput.ID) + if err != nil { + return fmt.Errorf("failed to get asset: %v", err) + } else if assetAsBytes != nil { + fmt.Println("Asset already exists: " + assetInput.ID) + return fmt.Errorf("this asset already exists: " + assetInput.ID) + } + + // Get ID of submitting client identity + clientID, err := submittingClientIdentity(ctx) + if err != nil { + return err + } + + // Verify that the client is submitting request to peer in their organization + // This is to ensure that a client from another org doesn't attempt to read or + // write private data from this peer. + err = verifyClientOrgMatchesPeerOrg(ctx) + if err != nil { + return fmt.Errorf("CreateAsset cannot be performed: Error %v", err) + } + + // Make submitting client the owner + asset := Asset{ + Type: assetInput.Type, + ID: assetInput.ID, + Color: assetInput.Color, + Size: assetInput.Size, + Owner: clientID, + } + assetJSONasBytes, err := json.Marshal(asset) + if err != nil { + return fmt.Errorf("failed to marshal asset into JSON: %v", err) + } + + // Save asset to private data collection + // Typical logger, logs to stdout/file in the fabric managed docker container, running this chaincode + // Look for container name like dev-peer0.org1.example.com-{chaincodename_version}-xyz + log.Printf("CreateAsset Put: collection %v, ID %v, owner %v", assetCollection, assetInput.ID, clientID) + + err = ctx.GetStub().PutPrivateData(assetCollection, assetInput.ID, assetJSONasBytes) + if err != nil { + return fmt.Errorf("failed to put asset into private data collecton: %v", err) + } + + // Save asset details to collection visible to owning organization + assetPrivateDetails := AssetPrivateDetails{ + ID: assetInput.ID, + AppraisedValue: assetInput.AppraisedValue, + } + + assetPrivateDetailsAsBytes, err := json.Marshal(assetPrivateDetails) // marshal asset details to JSON + if err != nil { + return fmt.Errorf("failed to marshal into JSON: %v", err) + } + + // Get collection name for this organization. + orgCollection, err := getCollectionName(ctx) + if err != nil { + return fmt.Errorf("failed to infer private collection name for the org: %v", err) + } + + // Put asset appraised value into owners org specific private data collection + log.Printf("Put: collection %v, ID %v", orgCollection, assetInput.ID) + err = ctx.GetStub().PutPrivateData(orgCollection, assetInput.ID, assetPrivateDetailsAsBytes) + if err != nil { + return fmt.Errorf("failed to put asset private details: %v", err) + } + return nil +} + +// AgreeToTransfer is used by the potential buyer of the asset to agree to the +// asset value. The agreed to appraisal value is stored in the buying orgs +// org specifc collection, while the the buyer client ID is stored in the asset collection +// using a composite key +func (s *SmartContract) AgreeToTransfer(ctx contractapi.TransactionContextInterface) error { + + // Get ID of submitting client identity + clientID, err := submittingClientIdentity(ctx) + if err != nil { + return err + } + + // Value is private, therefore it gets passed in transient field + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Persist the JSON bytes as-is so that there is no risk of nondeterministic marshaling. + valueJSONasBytes, ok := transientMap["asset_value"] + if !ok { + return fmt.Errorf("asset_value key not found in the transient map") + } + + // Unmarshal the tranisent map to get the asset ID. + var valueJSON AssetPrivateDetails + err = json.Unmarshal(valueJSONasBytes, &valueJSON) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + // Do some error checking since we get the chance + if len(valueJSON.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + if valueJSON.AppraisedValue <= 0 { + return fmt.Errorf("appraisedValue field must be a positive integer") + } + + // Read asset from the private data collection + asset, err := s.ReadAsset(ctx, valueJSON.ID) + if err != nil { + return fmt.Errorf("error reading asset: %v", err) + } + if asset == nil { + return fmt.Errorf("%v does not exist", valueJSON.ID) + } + // Verify that the client is submitting request to peer in their organization + err = verifyClientOrgMatchesPeerOrg(ctx) + if err != nil { + return fmt.Errorf("AgreeToTransfer cannot be performed: Error %v", err) + } + + // Get collection name for this organization. Needs to be read by a member of the organization. + orgCollection, err := getCollectionName(ctx) + if err != nil { + return fmt.Errorf("failed to infer private collection name for the org: %v", err) + } + + log.Printf("AgreeToTransfer Put: collection %v, ID %v", orgCollection, valueJSON.ID) + // Put agreed value in the org specifc private data collection + err = ctx.GetStub().PutPrivateData(orgCollection, valueJSON.ID, valueJSONasBytes) + if err != nil { + return fmt.Errorf("failed to put asset bid: %v", err) + } + + // Create agreeement that indicates which identity has agreed to purchase + // In a more realistic transfer scenario, a transfer agreement would be secured to ensure that it cannot + // be overwritten by another channel member + transferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{valueJSON.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + log.Printf("AgreeToTransfer Put: collection %v, ID %v, Key %v", assetCollection, valueJSON.ID, transferAgreeKey) + err = ctx.GetStub().PutPrivateData(assetCollection, transferAgreeKey, []byte(clientID)) + if err != nil { + return fmt.Errorf("failed to put asset bid: %v", err) + } + + return nil +} + +// TransferAsset transfers the asset to the new owner by setting a new owner ID +func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface) error { + + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient %v", err) + } + + // Asset properties are private, therefore they get passed in transient field + transientTransferJSON, ok := transientMap["asset_owner"] + if !ok { + return fmt.Errorf("asset owner not found in the transient map") + } + + type assetTransferTransientInput struct { + ID string `json:"assetID"` + BuyerMSP string `json:"buyerMSP"` + } + + var assetTransferInput assetTransferTransientInput + err = json.Unmarshal(transientTransferJSON, &assetTransferInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetTransferInput.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + if len(assetTransferInput.BuyerMSP) == 0 { + return fmt.Errorf("buyerMSP field must be a non-empty string") + } + log.Printf("TransferAsset: verify asset exists ID %v", assetTransferInput.ID) + // Read asset from the private data collection + asset, err := s.ReadAsset(ctx, assetTransferInput.ID) + if err != nil { + return fmt.Errorf("error reading asset: %v", err) + } + if asset == nil { + return fmt.Errorf("%v does not exist", assetTransferInput.ID) + } + // Verify that the client is submitting request to peer in their organization + err = verifyClientOrgMatchesPeerOrg(ctx) + if err != nil { + return fmt.Errorf("TransferAsset cannot be performed: Error %v", err) + } + + // Verify transfer details and transfer owner + err = s.verifyAgreement(ctx, assetTransferInput.ID, asset.Owner, assetTransferInput.BuyerMSP) + if err != nil { + return fmt.Errorf("failed transfer verification: %v", err) + } + + transferAgreement, err := s.ReadTransferAgreement(ctx, assetTransferInput.ID) + if err != nil { + return fmt.Errorf("failed ReadTransferAgreement to find buyerID: %v", err) + } + if transferAgreement.BuyerID == "" { + return fmt.Errorf("BuyerID not found in TransferAgreement for %v", assetTransferInput.ID) + } + + // Transfer asset in private data collection to new owner + asset.Owner = transferAgreement.BuyerID + + assetJSONasBytes, err := json.Marshal(asset) + if err != nil { + return fmt.Errorf("failed marshalling asset %v: %v", assetTransferInput.ID, err) + } + + log.Printf("TransferAsset Put: collection %v, ID %v", assetCollection, assetTransferInput.ID) + err = ctx.GetStub().PutPrivateData(assetCollection, assetTransferInput.ID, assetJSONasBytes) //rewrite the asset + if err != nil { + return err + } + + // Get collection name for this organization + ownersCollection, err := getCollectionName(ctx) + if err != nil { + return fmt.Errorf("failed to infer private collection name for the org: %v", err) + } + + // Delete the asset appraised value from this organization's private data collection + err = ctx.GetStub().DelPrivateData(ownersCollection, assetTransferInput.ID) + if err != nil { + return err + } + + // Delete the transfer agreement from the asset collection + transferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{assetTransferInput.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + err = ctx.GetStub().DelPrivateData(assetCollection, transferAgreeKey) + if err != nil { + return err + } + + return nil + +} + +// verifyAgreement is an internal helper function used by TransferAsset to verify +// that the transfer is being initiated by the owner and that the buyer has agreed +// to the same appraisal value as the owner +func (s *SmartContract) verifyAgreement(ctx contractapi.TransactionContextInterface, assetID string, owner string, buyerMSP string) error { + + // Check 1: verify that the transfer is being initiatied by the owner + + // Get ID of submitting client identity + clientID, err := submittingClientIdentity(ctx) + if err != nil { + return err + } + + if clientID != owner { + return fmt.Errorf("error: submitting client identity does not own asset") + } + + // Check 2: verify that the buyer has agreed to the appraised value + + // Get collection names + collectionOwner, err := getCollectionName(ctx) // get owner collection from caller identity + if err != nil { + return fmt.Errorf("failed to infer private collection name for the org: %v", err) + } + + collectionBuyer := buyerMSP + "PrivateCollection" // get buyers collection + + // Get hash of owners agreed to value + ownerAppraisedValueHash, err := ctx.GetStub().GetPrivateDataHash(collectionOwner, assetID) + if err != nil { + return fmt.Errorf("failed to get hash of appraised value from owners collection %v: %v", collectionOwner, err) + } + if ownerAppraisedValueHash == nil { + return fmt.Errorf("hash of appraised value for %v does not exist in collection %v", assetID, collectionOwner) + } + + // Get hash of buyers agreed to value + buyerAppraisedValueHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, assetID) + if err != nil { + return fmt.Errorf("failed to get hash of appraised value from buyer collection %v: %v", collectionBuyer, err) + } + if buyerAppraisedValueHash == nil { + return fmt.Errorf("hash of appraised value for %v does not exist in collection %v. AgreeToTransfer must be called by the buyer first", assetID, collectionBuyer) + } + + // Verify that the two hashes match + if !bytes.Equal(ownerAppraisedValueHash, buyerAppraisedValueHash) { + return fmt.Errorf("hash for appraised value for owner %x does not value for seller %x", ownerAppraisedValueHash, buyerAppraisedValueHash) + } + + return nil +} + +// DeleteAsset can be used by the owner of the asset to delete the asset +func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface) error { + + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("Error getting transient: %v", err) + } + + // Asset properties are private, therefore they get passed in transient field + transientDeleteJSON, ok := transientMap["asset_delete"] + if !ok { + return fmt.Errorf("asset to delete not found in the transient map") + } + + type assetDelete struct { + ID string `json:"assetID"` + } + + var assetDeleteInput assetDelete + err = json.Unmarshal(transientDeleteJSON, &assetDeleteInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetDeleteInput.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + + // Verify that the client is submitting request to peer in their organization + err = verifyClientOrgMatchesPeerOrg(ctx) + if err != nil { + return fmt.Errorf("DeleteAsset cannot be performed: Error %v", err) + } + + log.Printf("Deleting Asset: %v", assetDeleteInput.ID) + valAsbytes, err := ctx.GetStub().GetPrivateData(assetCollection, assetDeleteInput.ID) //get the asset from chaincode state + if err != nil { + return fmt.Errorf("failed to read asset: %v", err) + } + if valAsbytes == nil { + return fmt.Errorf("asset not found: %v", assetDeleteInput.ID) + } + + ownerCollection, err := getCollectionName(ctx) // Get owners collection + if err != nil { + return fmt.Errorf("failed to infer private collection name for the org: %v", err) + } + + //check the asset is in the caller org's private collection + valAsbytes, err = ctx.GetStub().GetPrivateData(ownerCollection, assetDeleteInput.ID) + if err != nil { + return fmt.Errorf("failed to read asset from owner's Collection: %v", err) + } + if valAsbytes == nil { + return fmt.Errorf("asset not found in owner's private Collection %v: %v", ownerCollection, assetDeleteInput.ID) + } + + // delete the asset from state + err = ctx.GetStub().DelPrivateData(assetCollection, assetDeleteInput.ID) + if err != nil { + return fmt.Errorf("failed to delete state: %v", err) + } + + // Finally, delete private details of asset + err = ctx.GetStub().DelPrivateData(ownerCollection, assetDeleteInput.ID) + if err != nil { + return err + } + + return nil + +} + +// DeleteTranferAgreement can be used by the buyer to withdraw a proposal from +// the asset collection and from his own collection. +func (s *SmartContract) DeleteTranferAgreement(ctx contractapi.TransactionContextInterface) error { + + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Asset properties are private, therefore they get passed in transient field + transientDeleteJSON, ok := transientMap["agreement_delete"] + if !ok { + return fmt.Errorf("asset to delete not found in the transient map") + } + + type assetDelete struct { + ID string `json:"assetID"` + } + + var assetDeleteInput assetDelete + err = json.Unmarshal(transientDeleteJSON, &assetDeleteInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetDeleteInput.ID) == 0 { + return fmt.Errorf("transient input ID field must be a non-empty string") + } + + // Verify that the client is submitting request to peer in their organization + err = verifyClientOrgMatchesPeerOrg(ctx) + if err != nil { + return fmt.Errorf("DeleteTranferAgreement cannot be performed: Error %v", err) + } + // Delete private details of agreement + orgCollection, err := getCollectionName(ctx) // Get proposers collection. + if err != nil { + return fmt.Errorf("failed to infer private collection name for the org: %v", err) + } + tranferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{assetDeleteInput. + ID}) // Create composite key + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + valAsbytes, err := ctx.GetStub().GetPrivateData(assetCollection, tranferAgreeKey) //get the transfer_agreement + if err != nil { + return fmt.Errorf("failed to read transfer_agreement: %v", err) + } + if valAsbytes == nil { + return fmt.Errorf("asset's transfer_agreement does not exist: %v", assetDeleteInput.ID) + } + + log.Printf("Deleting TranferAgreement: %v", assetDeleteInput.ID) + err = ctx.GetStub().DelPrivateData(orgCollection, assetDeleteInput.ID) // Delete the asset + if err != nil { + return err + } + + // Delete transfer agreement record + err = ctx.GetStub().DelPrivateData(assetCollection, tranferAgreeKey) // remove agreement from state + if err != nil { + return 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 := clientMSPID + "PrivateCollection" + + return orgCollection, nil +} + +// verifyClientOrgMatchesPeerOrg is an internal function used verify client org id and 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 submittingClientIdentity(ctx contractapi.TransactionContextInterface) (string, error) { + b64ID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return "", fmt.Errorf("Failed to read clientID: %v", err) + } + decodeID, err := base64.StdEncoding.DecodeString(b64ID) + if err != nil { + return "", fmt.Errorf("failed to base64 decode clientID: %v", err) + } + return string(decodeID), nil +} diff --git a/asset-transfer-private-data/chaincode-go/chaincode/asset_transfer_test.go b/asset-transfer-private-data/chaincode-go/chaincode/asset_transfer_test.go new file mode 100644 index 0000000..c6ec5ac --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/asset_transfer_test.go @@ -0,0 +1,444 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +package chaincode_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/hyperledger/fabric-chaincode-go/pkg/cid" + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" + + "github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go/chaincode" + "github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go/chaincode/mocks" + "github.com/stretchr/testify/require" +) + +/* +These unit tests use mocks to simulate chaincode-api & fabric interactions +The mocks are generated using counterfeiter directives in the comments (starting with "go:generate counterfeiter") +All files in mocks/* are generated by running following, in the directory with your directive: + `go generate` +*/ + +//go:generate counterfeiter -o mocks/transaction.go -fake-name TransactionContext . transactionContext +type transactionContext interface { + contractapi.TransactionContextInterface +} + +//go:generate counterfeiter -o mocks/chaincodestub.go -fake-name ChaincodeStub . chaincodeStub +type chaincodeStub interface { + shim.ChaincodeStubInterface +} + +//go:generate counterfeiter -o mocks/statequeryiterator.go -fake-name StateQueryIterator . stateQueryIterator +type stateQueryIterator interface { + shim.StateQueryIteratorInterface +} + +//go:generate counterfeiter -o mocks/clientIdentity.go -fake-name ClientIdentity . clientIdentity +type clientIdentity interface { + cid.ClientIdentity +} + +const assetCollectionName = "assetCollection" +const transferAgreementObjectType = "transferAgreement" +const myOrg1Msp = "Org1Testmsp" +const myOrg1Clientid = "myOrg1Userid" +const myOrg1PrivCollection = "Org1TestmspPrivateCollection" +const myOrg2Msp = "Org2Testmsp" +const myOrg2Clientid = "myOrg2Userid" +const myOrg2PrivCollection = "Org2TestmspPrivateCollection" + +type assetTransientInput struct { + Type string `json:"objectType"` + ID string `json:"assetID"` + Color string `json:"color"` + Size int `json:"size"` + AppraisedValue int `json:"appraisedValue"` +} + +type assetTransferTransientInput struct { + ID string `json:"assetID"` + BuyerMSP string `json:"buyerMSP"` +} + +func TestCreateAssetBadInput(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + + // No transient map + err := assetTransferCC.CreateAsset(transactionContext) + require.EqualError(t, err, "asset not found in the transient map input") + + // transient map with incomplete asset data + assetPropMap := map[string][]byte{ + "asset_properties": []byte("ill formatted property"), + } + chaincodeStub.GetTransientReturns(assetPropMap, nil) + err = assetTransferCC.CreateAsset(transactionContext) + require.Error(t, err, "Expected error: transient map with incomplete asset data") + require.Contains(t, err.Error(), "failed to unmarshal JSON") + + testAsset := &assetTransientInput{ + Type: "testfulasset", + } + setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) + err = assetTransferCC.CreateAsset(transactionContext) + require.EqualError(t, err, "assetID field must be a non-empty string") + + testAsset = &assetTransientInput{ + ID: "id1", + Color: "gray", + } + setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) + err = assetTransferCC.CreateAsset(transactionContext) + require.EqualError(t, err, "objectType field must be a non-empty string") + + // case when asset exists, GetPrivateData returns a valid data from ledger + testAsset = &assetTransientInput{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + AppraisedValue: 500, + } + setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) + chaincodeStub.GetPrivateDataReturns([]byte{}, nil) + err = assetTransferCC.CreateAsset(transactionContext) + require.EqualError(t, err, "this asset already exists: id1") +} + +func TestCreateAssetSuccessful(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + testAsset := &assetTransientInput{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + AppraisedValue: 500, + } + setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) + err := assetTransferCC.CreateAsset(transactionContext) + require.NoError(t, err) + //Validate PutPrivateData calls + calledCollection, calledId, _ := chaincodeStub.PutPrivateDataArgsForCall(0) + require.Equal(t, assetCollectionName, calledCollection) + require.Equal(t, "id1", calledId) + + expectedPrivateDetails := &chaincode.AssetPrivateDetails{ + ID: "id1", + AppraisedValue: 500, + } + assetBytes, err := json.Marshal(expectedPrivateDetails) + calledCollection, calledId, calledAssetBytes := chaincodeStub.PutPrivateDataArgsForCall(1) + require.Equal(t, myOrg1PrivCollection, calledCollection) + require.Equal(t, "id1", calledId) + require.Equal(t, assetBytes, calledAssetBytes) +} + +func TestAgreeToTransferBadInput(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + + assetPrivDetail := &chaincode.AssetPrivateDetails{ + ID: "id1", + //no AppraisedValue + } + setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) + origAsset := chaincode.Asset{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + Owner: myOrg1Clientid, + } + setReturnPrivateDataInStub(t, chaincodeStub, &origAsset) + + err := assetTransferCC.AgreeToTransfer(transactionContext) + require.EqualError(t, err, "appraisedValue field must be a positive integer") + + assetPrivDetail = &chaincode.AssetPrivateDetails{ + //no ID + AppraisedValue: 500, + } + setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) + err = assetTransferCC.AgreeToTransfer(transactionContext) + require.EqualError(t, err, "assetID field must be a non-empty string") + + assetPrivDetail = &chaincode.AssetPrivateDetails{ + ID: "id1", + AppraisedValue: 500, + } + setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) + //asset does not exist + setReturnPrivateDataInStub(t, chaincodeStub, nil) + err = assetTransferCC.AgreeToTransfer(transactionContext) + require.EqualError(t, err, "id1 does not exist") +} + +func TestAgreeToTransferSuccessful(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + assetPrivDetail := &chaincode.AssetPrivateDetails{ + ID: "id1", + AppraisedValue: 500, + } + setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) + origAsset := chaincode.Asset{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + Owner: myOrg1Clientid, + } + setReturnPrivateDataInStub(t, chaincodeStub, &origAsset) + chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) + err := assetTransferCC.AgreeToTransfer(transactionContext) + require.NoError(t, err) + + expectedDataBytes, err := json.Marshal(assetPrivDetail) + calledCollection, calledId, calledWithDataBytes := chaincodeStub.PutPrivateDataArgsForCall(0) + require.Equal(t, myOrg1PrivCollection, calledCollection) + require.Equal(t, "id1", calledId) + require.Equal(t, expectedDataBytes, calledWithDataBytes) + + calledCollection, calledId, calledWithDataBytes = chaincodeStub.PutPrivateDataArgsForCall(1) + require.Equal(t, assetCollectionName, calledCollection) + require.Equal(t, transferAgreementObjectType+"id1", calledId) + require.Equal(t, []byte(myOrg1Clientid), calledWithDataBytes) +} +func TestTransferAssetBadInput(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + + assetNewOwner := &assetTransferTransientInput{ + ID: "id1", + BuyerMSP: "", + } + setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) + setReturnPrivateDataInStub(t, chaincodeStub, &chaincode.Asset{}) + err := assetTransferCC.TransferAsset(transactionContext) + require.EqualError(t, err, "buyerMSP field must be a non-empty string") + + assetNewOwner = &assetTransferTransientInput{ + ID: "id1", + BuyerMSP: myOrg2Msp, + } + setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) + //asset does not exist + setReturnPrivateDataInStub(t, chaincodeStub, nil) + err = assetTransferCC.TransferAsset(transactionContext) + require.EqualError(t, err, "id1 does not exist") +} + +func TestTransferAssetSuccessful(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + assetNewOwner := &assetTransferTransientInput{ + ID: "id1", + BuyerMSP: myOrg2Msp, + } + setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) + origAsset := chaincode.Asset{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + Owner: myOrg1Clientid, + } + setReturnPrivateDataInStub(t, chaincodeStub, &origAsset) + //to ensure we pass data hash verification + chaincodeStub.GetPrivateDataHashReturns([]byte("datahash"), nil) + //to ensure that ReadTransferAgreement call returns org2 client ID + chaincodeStub.GetPrivateDataReturnsOnCall(1, []byte(myOrg2Clientid), nil) + chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) + + err := assetTransferCC.TransferAsset(transactionContext) + require.NoError(t, err) + //Validate PutPrivateData calls + expectedNewAsset := origAsset + expectedNewAsset.Owner = myOrg2Clientid + expectedNewAssetBytes, err := json.Marshal(expectedNewAsset) + require.NoError(t, err) + calledCollection, calledId, calledWithAssetBytes := chaincodeStub.PutPrivateDataArgsForCall(0) + require.Equal(t, assetCollectionName, calledCollection) + require.Equal(t, "id1", calledId) + require.Equal(t, expectedNewAssetBytes, calledWithAssetBytes) + calledCollection, calledId = chaincodeStub.DelPrivateDataArgsForCall(0) + require.Equal(t, myOrg1PrivCollection, calledCollection) + require.Equal(t, "id1", calledId) + + calledCollection, calledId = chaincodeStub.DelPrivateDataArgsForCall(1) + require.Equal(t, assetCollectionName, calledCollection) + require.Equal(t, transferAgreementObjectType+"id1", calledId) + +} + +func TestTransferAssetByNonOwner(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + assetNewOwner := &assetTransferTransientInput{ + ID: "id1", + BuyerMSP: myOrg1Msp, + } + setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) + //Try to transfer asset owned by Org2 + org2Asset := chaincode.Asset{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + Owner: myOrg2Clientid, + } + setReturnPrivateDataInStub(t, chaincodeStub, &org2Asset) + err := assetTransferCC.TransferAsset(transactionContext) + require.EqualError(t, err, "failed transfer verification: error: submitting client identity does not own asset") +} + +func TestTransferAssetWithoutAnAgreement(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + assetNewOwner := &assetTransferTransientInput{ + ID: "id1", + BuyerMSP: myOrg1Msp, + } + setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) + orgAsset := chaincode.Asset{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + Owner: myOrg1Clientid, + } + setReturnPrivateDataInStub(t, chaincodeStub, &orgAsset) + //to ensure we pass data hash verification + chaincodeStub.GetPrivateDataHashReturns([]byte("datahash"), nil) + chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) + //ReadTransferAgreement call returns no buyer client ID + chaincodeStub.GetPrivateDataReturnsOnCall(1, []byte{}, nil) + + err := assetTransferCC.TransferAsset(transactionContext) + require.EqualError(t, err, "BuyerID not found in TransferAgreement for id1") +} + +func TestTransferAssetNonMatchingAppraisalValue(t *testing.T) { + transactionContext, chaincodeStub := prepMocksAsOrg1() + assetTransferCC := chaincode.SmartContract{} + assetNewOwner := &assetTransferTransientInput{ + ID: "id1", + BuyerMSP: myOrg2Msp, + } + setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) + + orgAsset := chaincode.Asset{ + ID: "id1", + Type: "testfulasset", + Color: "gray", + Size: 7, + Owner: myOrg1Clientid, + } + setReturnPrivateDataInStub(t, chaincodeStub, &orgAsset) + chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) + //data hash different in each collection + chaincodeStub.GetPrivateDataHashReturnsOnCall(0, []byte("datahash1"), nil) + chaincodeStub.GetPrivateDataHashReturnsOnCall(1, []byte("datahash2"), nil) + + err := assetTransferCC.TransferAsset(transactionContext) + require.Error(t, err, "Expected failed hash verification") + require.Contains(t, err.Error(), "failed transfer verification: hash for appraised value") +} + +func prepMocksAsOrg1() (*mocks.TransactionContext, *mocks.ChaincodeStub) { + return prepMocks(myOrg1Msp, myOrg1Clientid) +} +func prepMocksAsOrg2() (*mocks.TransactionContext, *mocks.ChaincodeStub) { + return prepMocks(myOrg2Msp, myOrg2Clientid) +} +func prepMocks(orgMSP, clientId string) (*mocks.TransactionContext, *mocks.ChaincodeStub) { + chaincodeStub := &mocks.ChaincodeStub{} + transactionContext := &mocks.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + clientIdentity := &mocks.ClientIdentity{} + clientIdentity.GetMSPIDReturns(orgMSP, nil) + clientIdentity.GetIDReturns(clientId, nil) + //set matching msp ID using peer shim env variable + os.Setenv("CORE_PEER_LOCALMSPID", orgMSP) + transactionContext.GetClientIdentityReturns(clientIdentity) + return transactionContext, chaincodeStub +} + +func setReturnAssetPrivateDetailsInTransientMap(t *testing.T, chaincodeStub *mocks.ChaincodeStub, assetPrivDetail *chaincode.AssetPrivateDetails) []byte { + assetOwnerBytes := []byte{} + if assetPrivDetail != nil { + var err error + assetOwnerBytes, err = json.Marshal(assetPrivDetail) + require.NoError(t, err) + } + assetPropMap := map[string][]byte{ + "asset_value": assetOwnerBytes, + } + chaincodeStub.GetTransientReturns(assetPropMap, nil) + return assetOwnerBytes +} + +func setReturnAssetOwnerInTransientMap(t *testing.T, chaincodeStub *mocks.ChaincodeStub, assetOwner *assetTransferTransientInput) []byte { + assetOwnerBytes := []byte{} + if assetOwner != nil { + var err error + assetOwnerBytes, err = json.Marshal(assetOwner) + require.NoError(t, err) + } + assetPropMap := map[string][]byte{ + "asset_owner": assetOwnerBytes, + } + chaincodeStub.GetTransientReturns(assetPropMap, nil) + return assetOwnerBytes +} + +func setReturnAssetPropsInTransientMap(t *testing.T, chaincodeStub *mocks.ChaincodeStub, testAsset *assetTransientInput) []byte { + assetBytes := []byte{} + if testAsset != nil { + var err error + assetBytes, err = json.Marshal(testAsset) + require.NoError(t, err) + } + assetPropMap := map[string][]byte{ + "asset_properties": assetBytes, + } + chaincodeStub.GetTransientReturns(assetPropMap, nil) + return assetBytes +} + +func setReturnPrivateDataInStub(t *testing.T, chaincodeStub *mocks.ChaincodeStub, testAsset *chaincode.Asset) []byte { + if testAsset == nil { + chaincodeStub.GetPrivateDataReturns(nil, nil) + return nil + } else { + var err error + assetBytes, err := json.Marshal(testAsset) + require.NoError(t, err) + chaincodeStub.GetPrivateDataReturns(assetBytes, nil) + return assetBytes + } +} + +func setReturnAssetPrivateDetailsInStub(t *testing.T, chaincodeStub *mocks.ChaincodeStub, testAsset *chaincode.AssetPrivateDetails) []byte { + if testAsset == nil { + chaincodeStub.GetPrivateDataReturns(nil, nil) + return nil + } else { + var err error + assetBytes, err := json.Marshal(testAsset) + require.NoError(t, err) + chaincodeStub.GetPrivateDataReturns(assetBytes, nil) + return assetBytes + } +} diff --git a/asset-transfer-private-data/chaincode-go/chaincode/mocks/chaincodestub.go b/asset-transfer-private-data/chaincode-go/chaincode/mocks/chaincodestub.go new file mode 100644 index 0000000..eea9056 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/mocks/chaincodestub.go @@ -0,0 +1,2878 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-protos-go/peer" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type ChaincodeStub struct { + CreateCompositeKeyStub func(string, []string) (string, error) + createCompositeKeyMutex sync.RWMutex + createCompositeKeyArgsForCall []struct { + arg1 string + arg2 []string + } + createCompositeKeyReturns struct { + result1 string + result2 error + } + createCompositeKeyReturnsOnCall map[int]struct { + result1 string + result2 error + } + DelPrivateDataStub func(string, string) error + delPrivateDataMutex sync.RWMutex + delPrivateDataArgsForCall []struct { + arg1 string + arg2 string + } + delPrivateDataReturns struct { + result1 error + } + delPrivateDataReturnsOnCall map[int]struct { + result1 error + } + DelStateStub func(string) error + delStateMutex sync.RWMutex + delStateArgsForCall []struct { + arg1 string + } + delStateReturns struct { + result1 error + } + delStateReturnsOnCall map[int]struct { + result1 error + } + GetArgsStub func() [][]byte + getArgsMutex sync.RWMutex + getArgsArgsForCall []struct { + } + getArgsReturns struct { + result1 [][]byte + } + getArgsReturnsOnCall map[int]struct { + result1 [][]byte + } + GetArgsSliceStub func() ([]byte, error) + getArgsSliceMutex sync.RWMutex + getArgsSliceArgsForCall []struct { + } + getArgsSliceReturns struct { + result1 []byte + result2 error + } + getArgsSliceReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetBindingStub func() ([]byte, error) + getBindingMutex sync.RWMutex + getBindingArgsForCall []struct { + } + getBindingReturns struct { + result1 []byte + result2 error + } + getBindingReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetChannelIDStub func() string + getChannelIDMutex sync.RWMutex + getChannelIDArgsForCall []struct { + } + getChannelIDReturns struct { + result1 string + } + getChannelIDReturnsOnCall map[int]struct { + result1 string + } + GetCreatorStub func() ([]byte, error) + getCreatorMutex sync.RWMutex + getCreatorArgsForCall []struct { + } + getCreatorReturns struct { + result1 []byte + result2 error + } + getCreatorReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetDecorationsStub func() map[string][]byte + getDecorationsMutex sync.RWMutex + getDecorationsArgsForCall []struct { + } + getDecorationsReturns struct { + result1 map[string][]byte + } + getDecorationsReturnsOnCall map[int]struct { + result1 map[string][]byte + } + GetFunctionAndParametersStub func() (string, []string) + getFunctionAndParametersMutex sync.RWMutex + getFunctionAndParametersArgsForCall []struct { + } + getFunctionAndParametersReturns struct { + result1 string + result2 []string + } + getFunctionAndParametersReturnsOnCall map[int]struct { + result1 string + result2 []string + } + GetHistoryForKeyStub func(string) (shim.HistoryQueryIteratorInterface, error) + getHistoryForKeyMutex sync.RWMutex + getHistoryForKeyArgsForCall []struct { + arg1 string + } + getHistoryForKeyReturns struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + } + getHistoryForKeyReturnsOnCall map[int]struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + } + GetPrivateDataStub func(string, string) ([]byte, error) + getPrivateDataMutex sync.RWMutex + getPrivateDataArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataReturns struct { + result1 []byte + result2 error + } + getPrivateDataReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetPrivateDataByPartialCompositeKeyStub func(string, string, []string) (shim.StateQueryIteratorInterface, error) + getPrivateDataByPartialCompositeKeyMutex sync.RWMutex + getPrivateDataByPartialCompositeKeyArgsForCall []struct { + arg1 string + arg2 string + arg3 []string + } + getPrivateDataByPartialCompositeKeyReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getPrivateDataByPartialCompositeKeyReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetPrivateDataByRangeStub func(string, string, string) (shim.StateQueryIteratorInterface, error) + getPrivateDataByRangeMutex sync.RWMutex + getPrivateDataByRangeArgsForCall []struct { + arg1 string + arg2 string + arg3 string + } + getPrivateDataByRangeReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getPrivateDataByRangeReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetPrivateDataHashStub func(string, string) ([]byte, error) + getPrivateDataHashMutex sync.RWMutex + getPrivateDataHashArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataHashReturns struct { + result1 []byte + result2 error + } + getPrivateDataHashReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetPrivateDataQueryResultStub func(string, string) (shim.StateQueryIteratorInterface, error) + getPrivateDataQueryResultMutex sync.RWMutex + getPrivateDataQueryResultArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataQueryResultReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getPrivateDataQueryResultReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetPrivateDataValidationParameterStub func(string, string) ([]byte, error) + getPrivateDataValidationParameterMutex sync.RWMutex + getPrivateDataValidationParameterArgsForCall []struct { + arg1 string + arg2 string + } + getPrivateDataValidationParameterReturns struct { + result1 []byte + result2 error + } + getPrivateDataValidationParameterReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetQueryResultStub func(string) (shim.StateQueryIteratorInterface, error) + getQueryResultMutex sync.RWMutex + getQueryResultArgsForCall []struct { + arg1 string + } + getQueryResultReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getQueryResultReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetQueryResultWithPaginationStub func(string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) + getQueryResultWithPaginationMutex sync.RWMutex + getQueryResultWithPaginationArgsForCall []struct { + arg1 string + arg2 int32 + arg3 string + } + getQueryResultWithPaginationReturns struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + getQueryResultWithPaginationReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + GetSignedProposalStub func() (*peer.SignedProposal, error) + getSignedProposalMutex sync.RWMutex + getSignedProposalArgsForCall []struct { + } + getSignedProposalReturns struct { + result1 *peer.SignedProposal + result2 error + } + getSignedProposalReturnsOnCall map[int]struct { + result1 *peer.SignedProposal + result2 error + } + GetStateStub func(string) ([]byte, error) + getStateMutex sync.RWMutex + getStateArgsForCall []struct { + arg1 string + } + getStateReturns struct { + result1 []byte + result2 error + } + getStateReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetStateByPartialCompositeKeyStub func(string, []string) (shim.StateQueryIteratorInterface, error) + getStateByPartialCompositeKeyMutex sync.RWMutex + getStateByPartialCompositeKeyArgsForCall []struct { + arg1 string + arg2 []string + } + getStateByPartialCompositeKeyReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getStateByPartialCompositeKeyReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetStateByPartialCompositeKeyWithPaginationStub func(string, []string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) + getStateByPartialCompositeKeyWithPaginationMutex sync.RWMutex + getStateByPartialCompositeKeyWithPaginationArgsForCall []struct { + arg1 string + arg2 []string + arg3 int32 + arg4 string + } + getStateByPartialCompositeKeyWithPaginationReturns struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + getStateByPartialCompositeKeyWithPaginationReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + GetStateByRangeStub func(string, string) (shim.StateQueryIteratorInterface, error) + getStateByRangeMutex sync.RWMutex + getStateByRangeArgsForCall []struct { + arg1 string + arg2 string + } + getStateByRangeReturns struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + getStateByRangeReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + } + GetStateByRangeWithPaginationStub func(string, string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) + getStateByRangeWithPaginationMutex sync.RWMutex + getStateByRangeWithPaginationArgsForCall []struct { + arg1 string + arg2 string + arg3 int32 + arg4 string + } + getStateByRangeWithPaginationReturns struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + getStateByRangeWithPaginationReturnsOnCall map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + } + GetStateValidationParameterStub func(string) ([]byte, error) + getStateValidationParameterMutex sync.RWMutex + getStateValidationParameterArgsForCall []struct { + arg1 string + } + getStateValidationParameterReturns struct { + result1 []byte + result2 error + } + getStateValidationParameterReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + GetStringArgsStub func() []string + getStringArgsMutex sync.RWMutex + getStringArgsArgsForCall []struct { + } + getStringArgsReturns struct { + result1 []string + } + getStringArgsReturnsOnCall map[int]struct { + result1 []string + } + GetTransientStub func() (map[string][]byte, error) + getTransientMutex sync.RWMutex + getTransientArgsForCall []struct { + } + getTransientReturns struct { + result1 map[string][]byte + result2 error + } + getTransientReturnsOnCall map[int]struct { + result1 map[string][]byte + result2 error + } + GetTxIDStub func() string + getTxIDMutex sync.RWMutex + getTxIDArgsForCall []struct { + } + getTxIDReturns struct { + result1 string + } + getTxIDReturnsOnCall map[int]struct { + result1 string + } + GetTxTimestampStub func() (*timestamppb.Timestamp, error) + getTxTimestampMutex sync.RWMutex + getTxTimestampArgsForCall []struct { + } + getTxTimestampReturns struct { + result1 *timestamppb.Timestamp + result2 error + } + getTxTimestampReturnsOnCall map[int]struct { + result1 *timestamppb.Timestamp + result2 error + } + InvokeChaincodeStub func(string, [][]byte, string) peer.Response + invokeChaincodeMutex sync.RWMutex + invokeChaincodeArgsForCall []struct { + arg1 string + arg2 [][]byte + arg3 string + } + invokeChaincodeReturns struct { + result1 peer.Response + } + invokeChaincodeReturnsOnCall map[int]struct { + result1 peer.Response + } + PutPrivateDataStub func(string, string, []byte) error + putPrivateDataMutex sync.RWMutex + putPrivateDataArgsForCall []struct { + arg1 string + arg2 string + arg3 []byte + } + putPrivateDataReturns struct { + result1 error + } + putPrivateDataReturnsOnCall map[int]struct { + result1 error + } + PutStateStub func(string, []byte) error + putStateMutex sync.RWMutex + putStateArgsForCall []struct { + arg1 string + arg2 []byte + } + putStateReturns struct { + result1 error + } + putStateReturnsOnCall map[int]struct { + result1 error + } + SetEventStub func(string, []byte) error + setEventMutex sync.RWMutex + setEventArgsForCall []struct { + arg1 string + arg2 []byte + } + setEventReturns struct { + result1 error + } + setEventReturnsOnCall map[int]struct { + result1 error + } + SetPrivateDataValidationParameterStub func(string, string, []byte) error + setPrivateDataValidationParameterMutex sync.RWMutex + setPrivateDataValidationParameterArgsForCall []struct { + arg1 string + arg2 string + arg3 []byte + } + setPrivateDataValidationParameterReturns struct { + result1 error + } + setPrivateDataValidationParameterReturnsOnCall map[int]struct { + result1 error + } + SetStateValidationParameterStub func(string, []byte) error + setStateValidationParameterMutex sync.RWMutex + setStateValidationParameterArgsForCall []struct { + arg1 string + arg2 []byte + } + setStateValidationParameterReturns struct { + result1 error + } + setStateValidationParameterReturnsOnCall map[int]struct { + result1 error + } + SplitCompositeKeyStub func(string) (string, []string, error) + splitCompositeKeyMutex sync.RWMutex + splitCompositeKeyArgsForCall []struct { + arg1 string + } + splitCompositeKeyReturns struct { + result1 string + result2 []string + result3 error + } + splitCompositeKeyReturnsOnCall map[int]struct { + result1 string + result2 []string + result3 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *ChaincodeStub) CreateCompositeKey(arg1 string, arg2 []string) (string, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + fake.createCompositeKeyMutex.Lock() + ret, specificReturn := fake.createCompositeKeyReturnsOnCall[len(fake.createCompositeKeyArgsForCall)] + fake.createCompositeKeyArgsForCall = append(fake.createCompositeKeyArgsForCall, struct { + arg1 string + arg2 []string + }{arg1, arg2Copy}) + fake.recordInvocation("CreateCompositeKey", []interface{}{arg1, arg2Copy}) + fake.createCompositeKeyMutex.Unlock() + if fake.CreateCompositeKeyStub != nil { + return fake.CreateCompositeKeyStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.createCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) CreateCompositeKeyCallCount() int { + fake.createCompositeKeyMutex.RLock() + defer fake.createCompositeKeyMutex.RUnlock() + return len(fake.createCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) CreateCompositeKeyCalls(stub func(string, []string) (string, error)) { + fake.createCompositeKeyMutex.Lock() + defer fake.createCompositeKeyMutex.Unlock() + fake.CreateCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) CreateCompositeKeyArgsForCall(i int) (string, []string) { + fake.createCompositeKeyMutex.RLock() + defer fake.createCompositeKeyMutex.RUnlock() + argsForCall := fake.createCompositeKeyArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) CreateCompositeKeyReturns(result1 string, result2 error) { + fake.createCompositeKeyMutex.Lock() + defer fake.createCompositeKeyMutex.Unlock() + fake.CreateCompositeKeyStub = nil + fake.createCompositeKeyReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) CreateCompositeKeyReturnsOnCall(i int, result1 string, result2 error) { + fake.createCompositeKeyMutex.Lock() + defer fake.createCompositeKeyMutex.Unlock() + fake.CreateCompositeKeyStub = nil + if fake.createCompositeKeyReturnsOnCall == nil { + fake.createCompositeKeyReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.createCompositeKeyReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) DelPrivateData(arg1 string, arg2 string) error { + fake.delPrivateDataMutex.Lock() + ret, specificReturn := fake.delPrivateDataReturnsOnCall[len(fake.delPrivateDataArgsForCall)] + fake.delPrivateDataArgsForCall = append(fake.delPrivateDataArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("DelPrivateData", []interface{}{arg1, arg2}) + fake.delPrivateDataMutex.Unlock() + if fake.DelPrivateDataStub != nil { + return fake.DelPrivateDataStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.delPrivateDataReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) DelPrivateDataCallCount() int { + fake.delPrivateDataMutex.RLock() + defer fake.delPrivateDataMutex.RUnlock() + return len(fake.delPrivateDataArgsForCall) +} + +func (fake *ChaincodeStub) DelPrivateDataCalls(stub func(string, string) error) { + fake.delPrivateDataMutex.Lock() + defer fake.delPrivateDataMutex.Unlock() + fake.DelPrivateDataStub = stub +} + +func (fake *ChaincodeStub) DelPrivateDataArgsForCall(i int) (string, string) { + fake.delPrivateDataMutex.RLock() + defer fake.delPrivateDataMutex.RUnlock() + argsForCall := fake.delPrivateDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) DelPrivateDataReturns(result1 error) { + fake.delPrivateDataMutex.Lock() + defer fake.delPrivateDataMutex.Unlock() + fake.DelPrivateDataStub = nil + fake.delPrivateDataReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) DelPrivateDataReturnsOnCall(i int, result1 error) { + fake.delPrivateDataMutex.Lock() + defer fake.delPrivateDataMutex.Unlock() + fake.DelPrivateDataStub = nil + if fake.delPrivateDataReturnsOnCall == nil { + fake.delPrivateDataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.delPrivateDataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) DelState(arg1 string) error { + fake.delStateMutex.Lock() + ret, specificReturn := fake.delStateReturnsOnCall[len(fake.delStateArgsForCall)] + fake.delStateArgsForCall = append(fake.delStateArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("DelState", []interface{}{arg1}) + fake.delStateMutex.Unlock() + if fake.DelStateStub != nil { + return fake.DelStateStub(arg1) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.delStateReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) DelStateCallCount() int { + fake.delStateMutex.RLock() + defer fake.delStateMutex.RUnlock() + return len(fake.delStateArgsForCall) +} + +func (fake *ChaincodeStub) DelStateCalls(stub func(string) error) { + fake.delStateMutex.Lock() + defer fake.delStateMutex.Unlock() + fake.DelStateStub = stub +} + +func (fake *ChaincodeStub) DelStateArgsForCall(i int) string { + fake.delStateMutex.RLock() + defer fake.delStateMutex.RUnlock() + argsForCall := fake.delStateArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) DelStateReturns(result1 error) { + fake.delStateMutex.Lock() + defer fake.delStateMutex.Unlock() + fake.DelStateStub = nil + fake.delStateReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) DelStateReturnsOnCall(i int, result1 error) { + fake.delStateMutex.Lock() + defer fake.delStateMutex.Unlock() + fake.DelStateStub = nil + if fake.delStateReturnsOnCall == nil { + fake.delStateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.delStateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) GetArgs() [][]byte { + fake.getArgsMutex.Lock() + ret, specificReturn := fake.getArgsReturnsOnCall[len(fake.getArgsArgsForCall)] + fake.getArgsArgsForCall = append(fake.getArgsArgsForCall, struct { + }{}) + fake.recordInvocation("GetArgs", []interface{}{}) + fake.getArgsMutex.Unlock() + if fake.GetArgsStub != nil { + return fake.GetArgsStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getArgsReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetArgsCallCount() int { + fake.getArgsMutex.RLock() + defer fake.getArgsMutex.RUnlock() + return len(fake.getArgsArgsForCall) +} + +func (fake *ChaincodeStub) GetArgsCalls(stub func() [][]byte) { + fake.getArgsMutex.Lock() + defer fake.getArgsMutex.Unlock() + fake.GetArgsStub = stub +} + +func (fake *ChaincodeStub) GetArgsReturns(result1 [][]byte) { + fake.getArgsMutex.Lock() + defer fake.getArgsMutex.Unlock() + fake.GetArgsStub = nil + fake.getArgsReturns = struct { + result1 [][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetArgsReturnsOnCall(i int, result1 [][]byte) { + fake.getArgsMutex.Lock() + defer fake.getArgsMutex.Unlock() + fake.GetArgsStub = nil + if fake.getArgsReturnsOnCall == nil { + fake.getArgsReturnsOnCall = make(map[int]struct { + result1 [][]byte + }) + } + fake.getArgsReturnsOnCall[i] = struct { + result1 [][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetArgsSlice() ([]byte, error) { + fake.getArgsSliceMutex.Lock() + ret, specificReturn := fake.getArgsSliceReturnsOnCall[len(fake.getArgsSliceArgsForCall)] + fake.getArgsSliceArgsForCall = append(fake.getArgsSliceArgsForCall, struct { + }{}) + fake.recordInvocation("GetArgsSlice", []interface{}{}) + fake.getArgsSliceMutex.Unlock() + if fake.GetArgsSliceStub != nil { + return fake.GetArgsSliceStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getArgsSliceReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetArgsSliceCallCount() int { + fake.getArgsSliceMutex.RLock() + defer fake.getArgsSliceMutex.RUnlock() + return len(fake.getArgsSliceArgsForCall) +} + +func (fake *ChaincodeStub) GetArgsSliceCalls(stub func() ([]byte, error)) { + fake.getArgsSliceMutex.Lock() + defer fake.getArgsSliceMutex.Unlock() + fake.GetArgsSliceStub = stub +} + +func (fake *ChaincodeStub) GetArgsSliceReturns(result1 []byte, result2 error) { + fake.getArgsSliceMutex.Lock() + defer fake.getArgsSliceMutex.Unlock() + fake.GetArgsSliceStub = nil + fake.getArgsSliceReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetArgsSliceReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getArgsSliceMutex.Lock() + defer fake.getArgsSliceMutex.Unlock() + fake.GetArgsSliceStub = nil + if fake.getArgsSliceReturnsOnCall == nil { + fake.getArgsSliceReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getArgsSliceReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetBinding() ([]byte, error) { + fake.getBindingMutex.Lock() + ret, specificReturn := fake.getBindingReturnsOnCall[len(fake.getBindingArgsForCall)] + fake.getBindingArgsForCall = append(fake.getBindingArgsForCall, struct { + }{}) + fake.recordInvocation("GetBinding", []interface{}{}) + fake.getBindingMutex.Unlock() + if fake.GetBindingStub != nil { + return fake.GetBindingStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getBindingReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetBindingCallCount() int { + fake.getBindingMutex.RLock() + defer fake.getBindingMutex.RUnlock() + return len(fake.getBindingArgsForCall) +} + +func (fake *ChaincodeStub) GetBindingCalls(stub func() ([]byte, error)) { + fake.getBindingMutex.Lock() + defer fake.getBindingMutex.Unlock() + fake.GetBindingStub = stub +} + +func (fake *ChaincodeStub) GetBindingReturns(result1 []byte, result2 error) { + fake.getBindingMutex.Lock() + defer fake.getBindingMutex.Unlock() + fake.GetBindingStub = nil + fake.getBindingReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetBindingReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getBindingMutex.Lock() + defer fake.getBindingMutex.Unlock() + fake.GetBindingStub = nil + if fake.getBindingReturnsOnCall == nil { + fake.getBindingReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getBindingReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetChannelID() string { + fake.getChannelIDMutex.Lock() + ret, specificReturn := fake.getChannelIDReturnsOnCall[len(fake.getChannelIDArgsForCall)] + fake.getChannelIDArgsForCall = append(fake.getChannelIDArgsForCall, struct { + }{}) + fake.recordInvocation("GetChannelID", []interface{}{}) + fake.getChannelIDMutex.Unlock() + if fake.GetChannelIDStub != nil { + return fake.GetChannelIDStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getChannelIDReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetChannelIDCallCount() int { + fake.getChannelIDMutex.RLock() + defer fake.getChannelIDMutex.RUnlock() + return len(fake.getChannelIDArgsForCall) +} + +func (fake *ChaincodeStub) GetChannelIDCalls(stub func() string) { + fake.getChannelIDMutex.Lock() + defer fake.getChannelIDMutex.Unlock() + fake.GetChannelIDStub = stub +} + +func (fake *ChaincodeStub) GetChannelIDReturns(result1 string) { + fake.getChannelIDMutex.Lock() + defer fake.getChannelIDMutex.Unlock() + fake.GetChannelIDStub = nil + fake.getChannelIDReturns = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetChannelIDReturnsOnCall(i int, result1 string) { + fake.getChannelIDMutex.Lock() + defer fake.getChannelIDMutex.Unlock() + fake.GetChannelIDStub = nil + if fake.getChannelIDReturnsOnCall == nil { + fake.getChannelIDReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getChannelIDReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetCreator() ([]byte, error) { + fake.getCreatorMutex.Lock() + ret, specificReturn := fake.getCreatorReturnsOnCall[len(fake.getCreatorArgsForCall)] + fake.getCreatorArgsForCall = append(fake.getCreatorArgsForCall, struct { + }{}) + fake.recordInvocation("GetCreator", []interface{}{}) + fake.getCreatorMutex.Unlock() + if fake.GetCreatorStub != nil { + return fake.GetCreatorStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getCreatorReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetCreatorCallCount() int { + fake.getCreatorMutex.RLock() + defer fake.getCreatorMutex.RUnlock() + return len(fake.getCreatorArgsForCall) +} + +func (fake *ChaincodeStub) GetCreatorCalls(stub func() ([]byte, error)) { + fake.getCreatorMutex.Lock() + defer fake.getCreatorMutex.Unlock() + fake.GetCreatorStub = stub +} + +func (fake *ChaincodeStub) GetCreatorReturns(result1 []byte, result2 error) { + fake.getCreatorMutex.Lock() + defer fake.getCreatorMutex.Unlock() + fake.GetCreatorStub = nil + fake.getCreatorReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetCreatorReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getCreatorMutex.Lock() + defer fake.getCreatorMutex.Unlock() + fake.GetCreatorStub = nil + if fake.getCreatorReturnsOnCall == nil { + fake.getCreatorReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getCreatorReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetDecorations() map[string][]byte { + fake.getDecorationsMutex.Lock() + ret, specificReturn := fake.getDecorationsReturnsOnCall[len(fake.getDecorationsArgsForCall)] + fake.getDecorationsArgsForCall = append(fake.getDecorationsArgsForCall, struct { + }{}) + fake.recordInvocation("GetDecorations", []interface{}{}) + fake.getDecorationsMutex.Unlock() + if fake.GetDecorationsStub != nil { + return fake.GetDecorationsStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getDecorationsReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetDecorationsCallCount() int { + fake.getDecorationsMutex.RLock() + defer fake.getDecorationsMutex.RUnlock() + return len(fake.getDecorationsArgsForCall) +} + +func (fake *ChaincodeStub) GetDecorationsCalls(stub func() map[string][]byte) { + fake.getDecorationsMutex.Lock() + defer fake.getDecorationsMutex.Unlock() + fake.GetDecorationsStub = stub +} + +func (fake *ChaincodeStub) GetDecorationsReturns(result1 map[string][]byte) { + fake.getDecorationsMutex.Lock() + defer fake.getDecorationsMutex.Unlock() + fake.GetDecorationsStub = nil + fake.getDecorationsReturns = struct { + result1 map[string][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetDecorationsReturnsOnCall(i int, result1 map[string][]byte) { + fake.getDecorationsMutex.Lock() + defer fake.getDecorationsMutex.Unlock() + fake.GetDecorationsStub = nil + if fake.getDecorationsReturnsOnCall == nil { + fake.getDecorationsReturnsOnCall = make(map[int]struct { + result1 map[string][]byte + }) + } + fake.getDecorationsReturnsOnCall[i] = struct { + result1 map[string][]byte + }{result1} +} + +func (fake *ChaincodeStub) GetFunctionAndParameters() (string, []string) { + fake.getFunctionAndParametersMutex.Lock() + ret, specificReturn := fake.getFunctionAndParametersReturnsOnCall[len(fake.getFunctionAndParametersArgsForCall)] + fake.getFunctionAndParametersArgsForCall = append(fake.getFunctionAndParametersArgsForCall, struct { + }{}) + fake.recordInvocation("GetFunctionAndParameters", []interface{}{}) + fake.getFunctionAndParametersMutex.Unlock() + if fake.GetFunctionAndParametersStub != nil { + return fake.GetFunctionAndParametersStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getFunctionAndParametersReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetFunctionAndParametersCallCount() int { + fake.getFunctionAndParametersMutex.RLock() + defer fake.getFunctionAndParametersMutex.RUnlock() + return len(fake.getFunctionAndParametersArgsForCall) +} + +func (fake *ChaincodeStub) GetFunctionAndParametersCalls(stub func() (string, []string)) { + fake.getFunctionAndParametersMutex.Lock() + defer fake.getFunctionAndParametersMutex.Unlock() + fake.GetFunctionAndParametersStub = stub +} + +func (fake *ChaincodeStub) GetFunctionAndParametersReturns(result1 string, result2 []string) { + fake.getFunctionAndParametersMutex.Lock() + defer fake.getFunctionAndParametersMutex.Unlock() + fake.GetFunctionAndParametersStub = nil + fake.getFunctionAndParametersReturns = struct { + result1 string + result2 []string + }{result1, result2} +} + +func (fake *ChaincodeStub) GetFunctionAndParametersReturnsOnCall(i int, result1 string, result2 []string) { + fake.getFunctionAndParametersMutex.Lock() + defer fake.getFunctionAndParametersMutex.Unlock() + fake.GetFunctionAndParametersStub = nil + if fake.getFunctionAndParametersReturnsOnCall == nil { + fake.getFunctionAndParametersReturnsOnCall = make(map[int]struct { + result1 string + result2 []string + }) + } + fake.getFunctionAndParametersReturnsOnCall[i] = struct { + result1 string + result2 []string + }{result1, result2} +} + +func (fake *ChaincodeStub) GetHistoryForKey(arg1 string) (shim.HistoryQueryIteratorInterface, error) { + fake.getHistoryForKeyMutex.Lock() + ret, specificReturn := fake.getHistoryForKeyReturnsOnCall[len(fake.getHistoryForKeyArgsForCall)] + fake.getHistoryForKeyArgsForCall = append(fake.getHistoryForKeyArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetHistoryForKey", []interface{}{arg1}) + fake.getHistoryForKeyMutex.Unlock() + if fake.GetHistoryForKeyStub != nil { + return fake.GetHistoryForKeyStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getHistoryForKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetHistoryForKeyCallCount() int { + fake.getHistoryForKeyMutex.RLock() + defer fake.getHistoryForKeyMutex.RUnlock() + return len(fake.getHistoryForKeyArgsForCall) +} + +func (fake *ChaincodeStub) GetHistoryForKeyCalls(stub func(string) (shim.HistoryQueryIteratorInterface, error)) { + fake.getHistoryForKeyMutex.Lock() + defer fake.getHistoryForKeyMutex.Unlock() + fake.GetHistoryForKeyStub = stub +} + +func (fake *ChaincodeStub) GetHistoryForKeyArgsForCall(i int) string { + fake.getHistoryForKeyMutex.RLock() + defer fake.getHistoryForKeyMutex.RUnlock() + argsForCall := fake.getHistoryForKeyArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetHistoryForKeyReturns(result1 shim.HistoryQueryIteratorInterface, result2 error) { + fake.getHistoryForKeyMutex.Lock() + defer fake.getHistoryForKeyMutex.Unlock() + fake.GetHistoryForKeyStub = nil + fake.getHistoryForKeyReturns = struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetHistoryForKeyReturnsOnCall(i int, result1 shim.HistoryQueryIteratorInterface, result2 error) { + fake.getHistoryForKeyMutex.Lock() + defer fake.getHistoryForKeyMutex.Unlock() + fake.GetHistoryForKeyStub = nil + if fake.getHistoryForKeyReturnsOnCall == nil { + fake.getHistoryForKeyReturnsOnCall = make(map[int]struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + }) + } + fake.getHistoryForKeyReturnsOnCall[i] = struct { + result1 shim.HistoryQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateData(arg1 string, arg2 string) ([]byte, error) { + fake.getPrivateDataMutex.Lock() + ret, specificReturn := fake.getPrivateDataReturnsOnCall[len(fake.getPrivateDataArgsForCall)] + fake.getPrivateDataArgsForCall = append(fake.getPrivateDataArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateData", []interface{}{arg1, arg2}) + fake.getPrivateDataMutex.Unlock() + if fake.GetPrivateDataStub != nil { + return fake.GetPrivateDataStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataCallCount() int { + fake.getPrivateDataMutex.RLock() + defer fake.getPrivateDataMutex.RUnlock() + return len(fake.getPrivateDataArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataCalls(stub func(string, string) ([]byte, error)) { + fake.getPrivateDataMutex.Lock() + defer fake.getPrivateDataMutex.Unlock() + fake.GetPrivateDataStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataArgsForCall(i int) (string, string) { + fake.getPrivateDataMutex.RLock() + defer fake.getPrivateDataMutex.RUnlock() + argsForCall := fake.getPrivateDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataReturns(result1 []byte, result2 error) { + fake.getPrivateDataMutex.Lock() + defer fake.getPrivateDataMutex.Unlock() + fake.GetPrivateDataStub = nil + fake.getPrivateDataReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getPrivateDataMutex.Lock() + defer fake.getPrivateDataMutex.Unlock() + fake.GetPrivateDataStub = nil + if fake.getPrivateDataReturnsOnCall == nil { + fake.getPrivateDataReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getPrivateDataReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKey(arg1 string, arg2 string, arg3 []string) (shim.StateQueryIteratorInterface, error) { + var arg3Copy []string + if arg3 != nil { + arg3Copy = make([]string, len(arg3)) + copy(arg3Copy, arg3) + } + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + ret, specificReturn := fake.getPrivateDataByPartialCompositeKeyReturnsOnCall[len(fake.getPrivateDataByPartialCompositeKeyArgsForCall)] + fake.getPrivateDataByPartialCompositeKeyArgsForCall = append(fake.getPrivateDataByPartialCompositeKeyArgsForCall, struct { + arg1 string + arg2 string + arg3 []string + }{arg1, arg2, arg3Copy}) + fake.recordInvocation("GetPrivateDataByPartialCompositeKey", []interface{}{arg1, arg2, arg3Copy}) + fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + if fake.GetPrivateDataByPartialCompositeKeyStub != nil { + return fake.GetPrivateDataByPartialCompositeKeyStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataByPartialCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyCallCount() int { + fake.getPrivateDataByPartialCompositeKeyMutex.RLock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.RUnlock() + return len(fake.getPrivateDataByPartialCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyCalls(stub func(string, string, []string) (shim.StateQueryIteratorInterface, error)) { + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + fake.GetPrivateDataByPartialCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyArgsForCall(i int) (string, string, []string) { + fake.getPrivateDataByPartialCompositeKeyMutex.RLock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.RUnlock() + argsForCall := fake.getPrivateDataByPartialCompositeKeyArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + fake.GetPrivateDataByPartialCompositeKeyStub = nil + fake.getPrivateDataByPartialCompositeKeyReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByPartialCompositeKeyReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByPartialCompositeKeyMutex.Lock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.Unlock() + fake.GetPrivateDataByPartialCompositeKeyStub = nil + if fake.getPrivateDataByPartialCompositeKeyReturnsOnCall == nil { + fake.getPrivateDataByPartialCompositeKeyReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getPrivateDataByPartialCompositeKeyReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByRange(arg1 string, arg2 string, arg3 string) (shim.StateQueryIteratorInterface, error) { + fake.getPrivateDataByRangeMutex.Lock() + ret, specificReturn := fake.getPrivateDataByRangeReturnsOnCall[len(fake.getPrivateDataByRangeArgsForCall)] + fake.getPrivateDataByRangeArgsForCall = append(fake.getPrivateDataByRangeArgsForCall, struct { + arg1 string + arg2 string + arg3 string + }{arg1, arg2, arg3}) + fake.recordInvocation("GetPrivateDataByRange", []interface{}{arg1, arg2, arg3}) + fake.getPrivateDataByRangeMutex.Unlock() + if fake.GetPrivateDataByRangeStub != nil { + return fake.GetPrivateDataByRangeStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataByRangeReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeCallCount() int { + fake.getPrivateDataByRangeMutex.RLock() + defer fake.getPrivateDataByRangeMutex.RUnlock() + return len(fake.getPrivateDataByRangeArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeCalls(stub func(string, string, string) (shim.StateQueryIteratorInterface, error)) { + fake.getPrivateDataByRangeMutex.Lock() + defer fake.getPrivateDataByRangeMutex.Unlock() + fake.GetPrivateDataByRangeStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeArgsForCall(i int) (string, string, string) { + fake.getPrivateDataByRangeMutex.RLock() + defer fake.getPrivateDataByRangeMutex.RUnlock() + argsForCall := fake.getPrivateDataByRangeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByRangeMutex.Lock() + defer fake.getPrivateDataByRangeMutex.Unlock() + fake.GetPrivateDataByRangeStub = nil + fake.getPrivateDataByRangeReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataByRangeReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataByRangeMutex.Lock() + defer fake.getPrivateDataByRangeMutex.Unlock() + fake.GetPrivateDataByRangeStub = nil + if fake.getPrivateDataByRangeReturnsOnCall == nil { + fake.getPrivateDataByRangeReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getPrivateDataByRangeReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataHash(arg1 string, arg2 string) ([]byte, error) { + fake.getPrivateDataHashMutex.Lock() + ret, specificReturn := fake.getPrivateDataHashReturnsOnCall[len(fake.getPrivateDataHashArgsForCall)] + fake.getPrivateDataHashArgsForCall = append(fake.getPrivateDataHashArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateDataHash", []interface{}{arg1, arg2}) + fake.getPrivateDataHashMutex.Unlock() + if fake.GetPrivateDataHashStub != nil { + return fake.GetPrivateDataHashStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataHashReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataHashCallCount() int { + fake.getPrivateDataHashMutex.RLock() + defer fake.getPrivateDataHashMutex.RUnlock() + return len(fake.getPrivateDataHashArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataHashCalls(stub func(string, string) ([]byte, error)) { + fake.getPrivateDataHashMutex.Lock() + defer fake.getPrivateDataHashMutex.Unlock() + fake.GetPrivateDataHashStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataHashArgsForCall(i int) (string, string) { + fake.getPrivateDataHashMutex.RLock() + defer fake.getPrivateDataHashMutex.RUnlock() + argsForCall := fake.getPrivateDataHashArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataHashReturns(result1 []byte, result2 error) { + fake.getPrivateDataHashMutex.Lock() + defer fake.getPrivateDataHashMutex.Unlock() + fake.GetPrivateDataHashStub = nil + fake.getPrivateDataHashReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataHashReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getPrivateDataHashMutex.Lock() + defer fake.getPrivateDataHashMutex.Unlock() + fake.GetPrivateDataHashStub = nil + if fake.getPrivateDataHashReturnsOnCall == nil { + fake.getPrivateDataHashReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getPrivateDataHashReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResult(arg1 string, arg2 string) (shim.StateQueryIteratorInterface, error) { + fake.getPrivateDataQueryResultMutex.Lock() + ret, specificReturn := fake.getPrivateDataQueryResultReturnsOnCall[len(fake.getPrivateDataQueryResultArgsForCall)] + fake.getPrivateDataQueryResultArgsForCall = append(fake.getPrivateDataQueryResultArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateDataQueryResult", []interface{}{arg1, arg2}) + fake.getPrivateDataQueryResultMutex.Unlock() + if fake.GetPrivateDataQueryResultStub != nil { + return fake.GetPrivateDataQueryResultStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataQueryResultReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultCallCount() int { + fake.getPrivateDataQueryResultMutex.RLock() + defer fake.getPrivateDataQueryResultMutex.RUnlock() + return len(fake.getPrivateDataQueryResultArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultCalls(stub func(string, string) (shim.StateQueryIteratorInterface, error)) { + fake.getPrivateDataQueryResultMutex.Lock() + defer fake.getPrivateDataQueryResultMutex.Unlock() + fake.GetPrivateDataQueryResultStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultArgsForCall(i int) (string, string) { + fake.getPrivateDataQueryResultMutex.RLock() + defer fake.getPrivateDataQueryResultMutex.RUnlock() + argsForCall := fake.getPrivateDataQueryResultArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataQueryResultMutex.Lock() + defer fake.getPrivateDataQueryResultMutex.Unlock() + fake.GetPrivateDataQueryResultStub = nil + fake.getPrivateDataQueryResultReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataQueryResultReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getPrivateDataQueryResultMutex.Lock() + defer fake.getPrivateDataQueryResultMutex.Unlock() + fake.GetPrivateDataQueryResultStub = nil + if fake.getPrivateDataQueryResultReturnsOnCall == nil { + fake.getPrivateDataQueryResultReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getPrivateDataQueryResultReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameter(arg1 string, arg2 string) ([]byte, error) { + fake.getPrivateDataValidationParameterMutex.Lock() + ret, specificReturn := fake.getPrivateDataValidationParameterReturnsOnCall[len(fake.getPrivateDataValidationParameterArgsForCall)] + fake.getPrivateDataValidationParameterArgsForCall = append(fake.getPrivateDataValidationParameterArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetPrivateDataValidationParameter", []interface{}{arg1, arg2}) + fake.getPrivateDataValidationParameterMutex.Unlock() + if fake.GetPrivateDataValidationParameterStub != nil { + return fake.GetPrivateDataValidationParameterStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getPrivateDataValidationParameterReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterCallCount() int { + fake.getPrivateDataValidationParameterMutex.RLock() + defer fake.getPrivateDataValidationParameterMutex.RUnlock() + return len(fake.getPrivateDataValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterCalls(stub func(string, string) ([]byte, error)) { + fake.getPrivateDataValidationParameterMutex.Lock() + defer fake.getPrivateDataValidationParameterMutex.Unlock() + fake.GetPrivateDataValidationParameterStub = stub +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterArgsForCall(i int) (string, string) { + fake.getPrivateDataValidationParameterMutex.RLock() + defer fake.getPrivateDataValidationParameterMutex.RUnlock() + argsForCall := fake.getPrivateDataValidationParameterArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterReturns(result1 []byte, result2 error) { + fake.getPrivateDataValidationParameterMutex.Lock() + defer fake.getPrivateDataValidationParameterMutex.Unlock() + fake.GetPrivateDataValidationParameterStub = nil + fake.getPrivateDataValidationParameterReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetPrivateDataValidationParameterReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getPrivateDataValidationParameterMutex.Lock() + defer fake.getPrivateDataValidationParameterMutex.Unlock() + fake.GetPrivateDataValidationParameterStub = nil + if fake.getPrivateDataValidationParameterReturnsOnCall == nil { + fake.getPrivateDataValidationParameterReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getPrivateDataValidationParameterReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetQueryResult(arg1 string) (shim.StateQueryIteratorInterface, error) { + fake.getQueryResultMutex.Lock() + ret, specificReturn := fake.getQueryResultReturnsOnCall[len(fake.getQueryResultArgsForCall)] + fake.getQueryResultArgsForCall = append(fake.getQueryResultArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetQueryResult", []interface{}{arg1}) + fake.getQueryResultMutex.Unlock() + if fake.GetQueryResultStub != nil { + return fake.GetQueryResultStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getQueryResultReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetQueryResultCallCount() int { + fake.getQueryResultMutex.RLock() + defer fake.getQueryResultMutex.RUnlock() + return len(fake.getQueryResultArgsForCall) +} + +func (fake *ChaincodeStub) GetQueryResultCalls(stub func(string) (shim.StateQueryIteratorInterface, error)) { + fake.getQueryResultMutex.Lock() + defer fake.getQueryResultMutex.Unlock() + fake.GetQueryResultStub = stub +} + +func (fake *ChaincodeStub) GetQueryResultArgsForCall(i int) string { + fake.getQueryResultMutex.RLock() + defer fake.getQueryResultMutex.RUnlock() + argsForCall := fake.getQueryResultArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetQueryResultReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getQueryResultMutex.Lock() + defer fake.getQueryResultMutex.Unlock() + fake.GetQueryResultStub = nil + fake.getQueryResultReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetQueryResultReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getQueryResultMutex.Lock() + defer fake.getQueryResultMutex.Unlock() + fake.GetQueryResultStub = nil + if fake.getQueryResultReturnsOnCall == nil { + fake.getQueryResultReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getQueryResultReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetQueryResultWithPagination(arg1 string, arg2 int32, arg3 string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + fake.getQueryResultWithPaginationMutex.Lock() + ret, specificReturn := fake.getQueryResultWithPaginationReturnsOnCall[len(fake.getQueryResultWithPaginationArgsForCall)] + fake.getQueryResultWithPaginationArgsForCall = append(fake.getQueryResultWithPaginationArgsForCall, struct { + arg1 string + arg2 int32 + arg3 string + }{arg1, arg2, arg3}) + fake.recordInvocation("GetQueryResultWithPagination", []interface{}{arg1, arg2, arg3}) + fake.getQueryResultWithPaginationMutex.Unlock() + if fake.GetQueryResultWithPaginationStub != nil { + return fake.GetQueryResultWithPaginationStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getQueryResultWithPaginationReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationCallCount() int { + fake.getQueryResultWithPaginationMutex.RLock() + defer fake.getQueryResultWithPaginationMutex.RUnlock() + return len(fake.getQueryResultWithPaginationArgsForCall) +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationCalls(stub func(string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error)) { + fake.getQueryResultWithPaginationMutex.Lock() + defer fake.getQueryResultWithPaginationMutex.Unlock() + fake.GetQueryResultWithPaginationStub = stub +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationArgsForCall(i int) (string, int32, string) { + fake.getQueryResultWithPaginationMutex.RLock() + defer fake.getQueryResultWithPaginationMutex.RUnlock() + argsForCall := fake.getQueryResultWithPaginationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationReturns(result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getQueryResultWithPaginationMutex.Lock() + defer fake.getQueryResultWithPaginationMutex.Unlock() + fake.GetQueryResultWithPaginationStub = nil + fake.getQueryResultWithPaginationReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetQueryResultWithPaginationReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getQueryResultWithPaginationMutex.Lock() + defer fake.getQueryResultWithPaginationMutex.Unlock() + fake.GetQueryResultWithPaginationStub = nil + if fake.getQueryResultWithPaginationReturnsOnCall == nil { + fake.getQueryResultWithPaginationReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }) + } + fake.getQueryResultWithPaginationReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetSignedProposal() (*peer.SignedProposal, error) { + fake.getSignedProposalMutex.Lock() + ret, specificReturn := fake.getSignedProposalReturnsOnCall[len(fake.getSignedProposalArgsForCall)] + fake.getSignedProposalArgsForCall = append(fake.getSignedProposalArgsForCall, struct { + }{}) + fake.recordInvocation("GetSignedProposal", []interface{}{}) + fake.getSignedProposalMutex.Unlock() + if fake.GetSignedProposalStub != nil { + return fake.GetSignedProposalStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getSignedProposalReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetSignedProposalCallCount() int { + fake.getSignedProposalMutex.RLock() + defer fake.getSignedProposalMutex.RUnlock() + return len(fake.getSignedProposalArgsForCall) +} + +func (fake *ChaincodeStub) GetSignedProposalCalls(stub func() (*peer.SignedProposal, error)) { + fake.getSignedProposalMutex.Lock() + defer fake.getSignedProposalMutex.Unlock() + fake.GetSignedProposalStub = stub +} + +func (fake *ChaincodeStub) GetSignedProposalReturns(result1 *peer.SignedProposal, result2 error) { + fake.getSignedProposalMutex.Lock() + defer fake.getSignedProposalMutex.Unlock() + fake.GetSignedProposalStub = nil + fake.getSignedProposalReturns = struct { + result1 *peer.SignedProposal + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetSignedProposalReturnsOnCall(i int, result1 *peer.SignedProposal, result2 error) { + fake.getSignedProposalMutex.Lock() + defer fake.getSignedProposalMutex.Unlock() + fake.GetSignedProposalStub = nil + if fake.getSignedProposalReturnsOnCall == nil { + fake.getSignedProposalReturnsOnCall = make(map[int]struct { + result1 *peer.SignedProposal + result2 error + }) + } + fake.getSignedProposalReturnsOnCall[i] = struct { + result1 *peer.SignedProposal + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetState(arg1 string) ([]byte, error) { + fake.getStateMutex.Lock() + ret, specificReturn := fake.getStateReturnsOnCall[len(fake.getStateArgsForCall)] + fake.getStateArgsForCall = append(fake.getStateArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetState", []interface{}{arg1}) + fake.getStateMutex.Unlock() + if fake.GetStateStub != nil { + return fake.GetStateStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateCallCount() int { + fake.getStateMutex.RLock() + defer fake.getStateMutex.RUnlock() + return len(fake.getStateArgsForCall) +} + +func (fake *ChaincodeStub) GetStateCalls(stub func(string) ([]byte, error)) { + fake.getStateMutex.Lock() + defer fake.getStateMutex.Unlock() + fake.GetStateStub = stub +} + +func (fake *ChaincodeStub) GetStateArgsForCall(i int) string { + fake.getStateMutex.RLock() + defer fake.getStateMutex.RUnlock() + argsForCall := fake.getStateArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetStateReturns(result1 []byte, result2 error) { + fake.getStateMutex.Lock() + defer fake.getStateMutex.Unlock() + fake.GetStateStub = nil + fake.getStateReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getStateMutex.Lock() + defer fake.getStateMutex.Unlock() + fake.GetStateStub = nil + if fake.getStateReturnsOnCall == nil { + fake.getStateReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getStateReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKey(arg1 string, arg2 []string) (shim.StateQueryIteratorInterface, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + fake.getStateByPartialCompositeKeyMutex.Lock() + ret, specificReturn := fake.getStateByPartialCompositeKeyReturnsOnCall[len(fake.getStateByPartialCompositeKeyArgsForCall)] + fake.getStateByPartialCompositeKeyArgsForCall = append(fake.getStateByPartialCompositeKeyArgsForCall, struct { + arg1 string + arg2 []string + }{arg1, arg2Copy}) + fake.recordInvocation("GetStateByPartialCompositeKey", []interface{}{arg1, arg2Copy}) + fake.getStateByPartialCompositeKeyMutex.Unlock() + if fake.GetStateByPartialCompositeKeyStub != nil { + return fake.GetStateByPartialCompositeKeyStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateByPartialCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyCallCount() int { + fake.getStateByPartialCompositeKeyMutex.RLock() + defer fake.getStateByPartialCompositeKeyMutex.RUnlock() + return len(fake.getStateByPartialCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyCalls(stub func(string, []string) (shim.StateQueryIteratorInterface, error)) { + fake.getStateByPartialCompositeKeyMutex.Lock() + defer fake.getStateByPartialCompositeKeyMutex.Unlock() + fake.GetStateByPartialCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyArgsForCall(i int) (string, []string) { + fake.getStateByPartialCompositeKeyMutex.RLock() + defer fake.getStateByPartialCompositeKeyMutex.RUnlock() + argsForCall := fake.getStateByPartialCompositeKeyArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByPartialCompositeKeyMutex.Lock() + defer fake.getStateByPartialCompositeKeyMutex.Unlock() + fake.GetStateByPartialCompositeKeyStub = nil + fake.getStateByPartialCompositeKeyReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByPartialCompositeKeyMutex.Lock() + defer fake.getStateByPartialCompositeKeyMutex.Unlock() + fake.GetStateByPartialCompositeKeyStub = nil + if fake.getStateByPartialCompositeKeyReturnsOnCall == nil { + fake.getStateByPartialCompositeKeyReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getStateByPartialCompositeKeyReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPagination(arg1 string, arg2 []string, arg3 int32, arg4 string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + ret, specificReturn := fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall[len(fake.getStateByPartialCompositeKeyWithPaginationArgsForCall)] + fake.getStateByPartialCompositeKeyWithPaginationArgsForCall = append(fake.getStateByPartialCompositeKeyWithPaginationArgsForCall, struct { + arg1 string + arg2 []string + arg3 int32 + arg4 string + }{arg1, arg2Copy, arg3, arg4}) + fake.recordInvocation("GetStateByPartialCompositeKeyWithPagination", []interface{}{arg1, arg2Copy, arg3, arg4}) + fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + if fake.GetStateByPartialCompositeKeyWithPaginationStub != nil { + return fake.GetStateByPartialCompositeKeyWithPaginationStub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getStateByPartialCompositeKeyWithPaginationReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationCallCount() int { + fake.getStateByPartialCompositeKeyWithPaginationMutex.RLock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.RUnlock() + return len(fake.getStateByPartialCompositeKeyWithPaginationArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationCalls(stub func(string, []string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error)) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + fake.GetStateByPartialCompositeKeyWithPaginationStub = stub +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationArgsForCall(i int) (string, []string, int32, string) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.RLock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.RUnlock() + argsForCall := fake.getStateByPartialCompositeKeyWithPaginationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationReturns(result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + fake.GetStateByPartialCompositeKeyWithPaginationStub = nil + fake.getStateByPartialCompositeKeyWithPaginationReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateByPartialCompositeKeyWithPaginationReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByPartialCompositeKeyWithPaginationMutex.Lock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.Unlock() + fake.GetStateByPartialCompositeKeyWithPaginationStub = nil + if fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall == nil { + fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }) + } + fake.getStateByPartialCompositeKeyWithPaginationReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateByRange(arg1 string, arg2 string) (shim.StateQueryIteratorInterface, error) { + fake.getStateByRangeMutex.Lock() + ret, specificReturn := fake.getStateByRangeReturnsOnCall[len(fake.getStateByRangeArgsForCall)] + fake.getStateByRangeArgsForCall = append(fake.getStateByRangeArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("GetStateByRange", []interface{}{arg1, arg2}) + fake.getStateByRangeMutex.Unlock() + if fake.GetStateByRangeStub != nil { + return fake.GetStateByRangeStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateByRangeReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateByRangeCallCount() int { + fake.getStateByRangeMutex.RLock() + defer fake.getStateByRangeMutex.RUnlock() + return len(fake.getStateByRangeArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByRangeCalls(stub func(string, string) (shim.StateQueryIteratorInterface, error)) { + fake.getStateByRangeMutex.Lock() + defer fake.getStateByRangeMutex.Unlock() + fake.GetStateByRangeStub = stub +} + +func (fake *ChaincodeStub) GetStateByRangeArgsForCall(i int) (string, string) { + fake.getStateByRangeMutex.RLock() + defer fake.getStateByRangeMutex.RUnlock() + argsForCall := fake.getStateByRangeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) GetStateByRangeReturns(result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByRangeMutex.Lock() + defer fake.getStateByRangeMutex.Unlock() + fake.GetStateByRangeStub = nil + fake.getStateByRangeReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByRangeReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 error) { + fake.getStateByRangeMutex.Lock() + defer fake.getStateByRangeMutex.Unlock() + fake.GetStateByRangeStub = nil + if fake.getStateByRangeReturnsOnCall == nil { + fake.getStateByRangeReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 error + }) + } + fake.getStateByRangeReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateByRangeWithPagination(arg1 string, arg2 string, arg3 int32, arg4 string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + fake.getStateByRangeWithPaginationMutex.Lock() + ret, specificReturn := fake.getStateByRangeWithPaginationReturnsOnCall[len(fake.getStateByRangeWithPaginationArgsForCall)] + fake.getStateByRangeWithPaginationArgsForCall = append(fake.getStateByRangeWithPaginationArgsForCall, struct { + arg1 string + arg2 string + arg3 int32 + arg4 string + }{arg1, arg2, arg3, arg4}) + fake.recordInvocation("GetStateByRangeWithPagination", []interface{}{arg1, arg2, arg3, arg4}) + fake.getStateByRangeWithPaginationMutex.Unlock() + if fake.GetStateByRangeWithPaginationStub != nil { + return fake.GetStateByRangeWithPaginationStub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getStateByRangeWithPaginationReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationCallCount() int { + fake.getStateByRangeWithPaginationMutex.RLock() + defer fake.getStateByRangeWithPaginationMutex.RUnlock() + return len(fake.getStateByRangeWithPaginationArgsForCall) +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationCalls(stub func(string, string, int32, string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error)) { + fake.getStateByRangeWithPaginationMutex.Lock() + defer fake.getStateByRangeWithPaginationMutex.Unlock() + fake.GetStateByRangeWithPaginationStub = stub +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationArgsForCall(i int) (string, string, int32, string) { + fake.getStateByRangeWithPaginationMutex.RLock() + defer fake.getStateByRangeWithPaginationMutex.RUnlock() + argsForCall := fake.getStateByRangeWithPaginationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationReturns(result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByRangeWithPaginationMutex.Lock() + defer fake.getStateByRangeWithPaginationMutex.Unlock() + fake.GetStateByRangeWithPaginationStub = nil + fake.getStateByRangeWithPaginationReturns = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateByRangeWithPaginationReturnsOnCall(i int, result1 shim.StateQueryIteratorInterface, result2 *peer.QueryResponseMetadata, result3 error) { + fake.getStateByRangeWithPaginationMutex.Lock() + defer fake.getStateByRangeWithPaginationMutex.Unlock() + fake.GetStateByRangeWithPaginationStub = nil + if fake.getStateByRangeWithPaginationReturnsOnCall == nil { + fake.getStateByRangeWithPaginationReturnsOnCall = make(map[int]struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }) + } + fake.getStateByRangeWithPaginationReturnsOnCall[i] = struct { + result1 shim.StateQueryIteratorInterface + result2 *peer.QueryResponseMetadata + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) GetStateValidationParameter(arg1 string) ([]byte, error) { + fake.getStateValidationParameterMutex.Lock() + ret, specificReturn := fake.getStateValidationParameterReturnsOnCall[len(fake.getStateValidationParameterArgsForCall)] + fake.getStateValidationParameterArgsForCall = append(fake.getStateValidationParameterArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetStateValidationParameter", []interface{}{arg1}) + fake.getStateValidationParameterMutex.Unlock() + if fake.GetStateValidationParameterStub != nil { + return fake.GetStateValidationParameterStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getStateValidationParameterReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetStateValidationParameterCallCount() int { + fake.getStateValidationParameterMutex.RLock() + defer fake.getStateValidationParameterMutex.RUnlock() + return len(fake.getStateValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) GetStateValidationParameterCalls(stub func(string) ([]byte, error)) { + fake.getStateValidationParameterMutex.Lock() + defer fake.getStateValidationParameterMutex.Unlock() + fake.GetStateValidationParameterStub = stub +} + +func (fake *ChaincodeStub) GetStateValidationParameterArgsForCall(i int) string { + fake.getStateValidationParameterMutex.RLock() + defer fake.getStateValidationParameterMutex.RUnlock() + argsForCall := fake.getStateValidationParameterArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) GetStateValidationParameterReturns(result1 []byte, result2 error) { + fake.getStateValidationParameterMutex.Lock() + defer fake.getStateValidationParameterMutex.Unlock() + fake.GetStateValidationParameterStub = nil + fake.getStateValidationParameterReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStateValidationParameterReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getStateValidationParameterMutex.Lock() + defer fake.getStateValidationParameterMutex.Unlock() + fake.GetStateValidationParameterStub = nil + if fake.getStateValidationParameterReturnsOnCall == nil { + fake.getStateValidationParameterReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getStateValidationParameterReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetStringArgs() []string { + fake.getStringArgsMutex.Lock() + ret, specificReturn := fake.getStringArgsReturnsOnCall[len(fake.getStringArgsArgsForCall)] + fake.getStringArgsArgsForCall = append(fake.getStringArgsArgsForCall, struct { + }{}) + fake.recordInvocation("GetStringArgs", []interface{}{}) + fake.getStringArgsMutex.Unlock() + if fake.GetStringArgsStub != nil { + return fake.GetStringArgsStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getStringArgsReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetStringArgsCallCount() int { + fake.getStringArgsMutex.RLock() + defer fake.getStringArgsMutex.RUnlock() + return len(fake.getStringArgsArgsForCall) +} + +func (fake *ChaincodeStub) GetStringArgsCalls(stub func() []string) { + fake.getStringArgsMutex.Lock() + defer fake.getStringArgsMutex.Unlock() + fake.GetStringArgsStub = stub +} + +func (fake *ChaincodeStub) GetStringArgsReturns(result1 []string) { + fake.getStringArgsMutex.Lock() + defer fake.getStringArgsMutex.Unlock() + fake.GetStringArgsStub = nil + fake.getStringArgsReturns = struct { + result1 []string + }{result1} +} + +func (fake *ChaincodeStub) GetStringArgsReturnsOnCall(i int, result1 []string) { + fake.getStringArgsMutex.Lock() + defer fake.getStringArgsMutex.Unlock() + fake.GetStringArgsStub = nil + if fake.getStringArgsReturnsOnCall == nil { + fake.getStringArgsReturnsOnCall = make(map[int]struct { + result1 []string + }) + } + fake.getStringArgsReturnsOnCall[i] = struct { + result1 []string + }{result1} +} + +func (fake *ChaincodeStub) GetTransient() (map[string][]byte, error) { + fake.getTransientMutex.Lock() + ret, specificReturn := fake.getTransientReturnsOnCall[len(fake.getTransientArgsForCall)] + fake.getTransientArgsForCall = append(fake.getTransientArgsForCall, struct { + }{}) + fake.recordInvocation("GetTransient", []interface{}{}) + fake.getTransientMutex.Unlock() + if fake.GetTransientStub != nil { + return fake.GetTransientStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getTransientReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetTransientCallCount() int { + fake.getTransientMutex.RLock() + defer fake.getTransientMutex.RUnlock() + return len(fake.getTransientArgsForCall) +} + +func (fake *ChaincodeStub) GetTransientCalls(stub func() (map[string][]byte, error)) { + fake.getTransientMutex.Lock() + defer fake.getTransientMutex.Unlock() + fake.GetTransientStub = stub +} + +func (fake *ChaincodeStub) GetTransientReturns(result1 map[string][]byte, result2 error) { + fake.getTransientMutex.Lock() + defer fake.getTransientMutex.Unlock() + fake.GetTransientStub = nil + fake.getTransientReturns = struct { + result1 map[string][]byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetTransientReturnsOnCall(i int, result1 map[string][]byte, result2 error) { + fake.getTransientMutex.Lock() + defer fake.getTransientMutex.Unlock() + fake.GetTransientStub = nil + if fake.getTransientReturnsOnCall == nil { + fake.getTransientReturnsOnCall = make(map[int]struct { + result1 map[string][]byte + result2 error + }) + } + fake.getTransientReturnsOnCall[i] = struct { + result1 map[string][]byte + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetTxID() string { + fake.getTxIDMutex.Lock() + ret, specificReturn := fake.getTxIDReturnsOnCall[len(fake.getTxIDArgsForCall)] + fake.getTxIDArgsForCall = append(fake.getTxIDArgsForCall, struct { + }{}) + fake.recordInvocation("GetTxID", []interface{}{}) + fake.getTxIDMutex.Unlock() + if fake.GetTxIDStub != nil { + return fake.GetTxIDStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getTxIDReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) GetTxIDCallCount() int { + fake.getTxIDMutex.RLock() + defer fake.getTxIDMutex.RUnlock() + return len(fake.getTxIDArgsForCall) +} + +func (fake *ChaincodeStub) GetTxIDCalls(stub func() string) { + fake.getTxIDMutex.Lock() + defer fake.getTxIDMutex.Unlock() + fake.GetTxIDStub = stub +} + +func (fake *ChaincodeStub) GetTxIDReturns(result1 string) { + fake.getTxIDMutex.Lock() + defer fake.getTxIDMutex.Unlock() + fake.GetTxIDStub = nil + fake.getTxIDReturns = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetTxIDReturnsOnCall(i int, result1 string) { + fake.getTxIDMutex.Lock() + defer fake.getTxIDMutex.Unlock() + fake.GetTxIDStub = nil + if fake.getTxIDReturnsOnCall == nil { + fake.getTxIDReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getTxIDReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *ChaincodeStub) GetTxTimestamp() (*timestamppb.Timestamp, error) { + fake.getTxTimestampMutex.Lock() + ret, specificReturn := fake.getTxTimestampReturnsOnCall[len(fake.getTxTimestampArgsForCall)] + fake.getTxTimestampArgsForCall = append(fake.getTxTimestampArgsForCall, struct { + }{}) + fake.recordInvocation("GetTxTimestamp", []interface{}{}) + fake.getTxTimestampMutex.Unlock() + if fake.GetTxTimestampStub != nil { + return fake.GetTxTimestampStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getTxTimestampReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChaincodeStub) GetTxTimestampCallCount() int { + fake.getTxTimestampMutex.RLock() + defer fake.getTxTimestampMutex.RUnlock() + return len(fake.getTxTimestampArgsForCall) +} + +func (fake *ChaincodeStub) GetTxTimestampCalls(stub func() (*timestamppb.Timestamp, error)) { + fake.getTxTimestampMutex.Lock() + defer fake.getTxTimestampMutex.Unlock() + fake.GetTxTimestampStub = stub +} + +func (fake *ChaincodeStub) GetTxTimestampReturns(result1 *timestamppb.Timestamp, result2 error) { + fake.getTxTimestampMutex.Lock() + defer fake.getTxTimestampMutex.Unlock() + fake.GetTxTimestampStub = nil + fake.getTxTimestampReturns = struct { + result1 *timestamppb.Timestamp + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) GetTxTimestampReturnsOnCall(i int, result1 *timestamppb.Timestamp, result2 error) { + fake.getTxTimestampMutex.Lock() + defer fake.getTxTimestampMutex.Unlock() + fake.GetTxTimestampStub = nil + if fake.getTxTimestampReturnsOnCall == nil { + fake.getTxTimestampReturnsOnCall = make(map[int]struct { + result1 *timestamppb.Timestamp + result2 error + }) + } + fake.getTxTimestampReturnsOnCall[i] = struct { + result1 *timestamppb.Timestamp + result2 error + }{result1, result2} +} + +func (fake *ChaincodeStub) InvokeChaincode(arg1 string, arg2 [][]byte, arg3 string) peer.Response { + var arg2Copy [][]byte + if arg2 != nil { + arg2Copy = make([][]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.invokeChaincodeMutex.Lock() + ret, specificReturn := fake.invokeChaincodeReturnsOnCall[len(fake.invokeChaincodeArgsForCall)] + fake.invokeChaincodeArgsForCall = append(fake.invokeChaincodeArgsForCall, struct { + arg1 string + arg2 [][]byte + arg3 string + }{arg1, arg2Copy, arg3}) + fake.recordInvocation("InvokeChaincode", []interface{}{arg1, arg2Copy, arg3}) + fake.invokeChaincodeMutex.Unlock() + if fake.InvokeChaincodeStub != nil { + return fake.InvokeChaincodeStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.invokeChaincodeReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) InvokeChaincodeCallCount() int { + fake.invokeChaincodeMutex.RLock() + defer fake.invokeChaincodeMutex.RUnlock() + return len(fake.invokeChaincodeArgsForCall) +} + +func (fake *ChaincodeStub) InvokeChaincodeCalls(stub func(string, [][]byte, string) peer.Response) { + fake.invokeChaincodeMutex.Lock() + defer fake.invokeChaincodeMutex.Unlock() + fake.InvokeChaincodeStub = stub +} + +func (fake *ChaincodeStub) InvokeChaincodeArgsForCall(i int) (string, [][]byte, string) { + fake.invokeChaincodeMutex.RLock() + defer fake.invokeChaincodeMutex.RUnlock() + argsForCall := fake.invokeChaincodeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) InvokeChaincodeReturns(result1 peer.Response) { + fake.invokeChaincodeMutex.Lock() + defer fake.invokeChaincodeMutex.Unlock() + fake.InvokeChaincodeStub = nil + fake.invokeChaincodeReturns = struct { + result1 peer.Response + }{result1} +} + +func (fake *ChaincodeStub) InvokeChaincodeReturnsOnCall(i int, result1 peer.Response) { + fake.invokeChaincodeMutex.Lock() + defer fake.invokeChaincodeMutex.Unlock() + fake.InvokeChaincodeStub = nil + if fake.invokeChaincodeReturnsOnCall == nil { + fake.invokeChaincodeReturnsOnCall = make(map[int]struct { + result1 peer.Response + }) + } + fake.invokeChaincodeReturnsOnCall[i] = struct { + result1 peer.Response + }{result1} +} + +func (fake *ChaincodeStub) PutPrivateData(arg1 string, arg2 string, arg3 []byte) error { + var arg3Copy []byte + if arg3 != nil { + arg3Copy = make([]byte, len(arg3)) + copy(arg3Copy, arg3) + } + fake.putPrivateDataMutex.Lock() + ret, specificReturn := fake.putPrivateDataReturnsOnCall[len(fake.putPrivateDataArgsForCall)] + fake.putPrivateDataArgsForCall = append(fake.putPrivateDataArgsForCall, struct { + arg1 string + arg2 string + arg3 []byte + }{arg1, arg2, arg3Copy}) + fake.recordInvocation("PutPrivateData", []interface{}{arg1, arg2, arg3Copy}) + fake.putPrivateDataMutex.Unlock() + if fake.PutPrivateDataStub != nil { + return fake.PutPrivateDataStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.putPrivateDataReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) PutPrivateDataCallCount() int { + fake.putPrivateDataMutex.RLock() + defer fake.putPrivateDataMutex.RUnlock() + return len(fake.putPrivateDataArgsForCall) +} + +func (fake *ChaincodeStub) PutPrivateDataCalls(stub func(string, string, []byte) error) { + fake.putPrivateDataMutex.Lock() + defer fake.putPrivateDataMutex.Unlock() + fake.PutPrivateDataStub = stub +} + +func (fake *ChaincodeStub) PutPrivateDataArgsForCall(i int) (string, string, []byte) { + fake.putPrivateDataMutex.RLock() + defer fake.putPrivateDataMutex.RUnlock() + argsForCall := fake.putPrivateDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) PutPrivateDataReturns(result1 error) { + fake.putPrivateDataMutex.Lock() + defer fake.putPrivateDataMutex.Unlock() + fake.PutPrivateDataStub = nil + fake.putPrivateDataReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) PutPrivateDataReturnsOnCall(i int, result1 error) { + fake.putPrivateDataMutex.Lock() + defer fake.putPrivateDataMutex.Unlock() + fake.PutPrivateDataStub = nil + if fake.putPrivateDataReturnsOnCall == nil { + fake.putPrivateDataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.putPrivateDataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) PutState(arg1 string, arg2 []byte) error { + var arg2Copy []byte + if arg2 != nil { + arg2Copy = make([]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.putStateMutex.Lock() + ret, specificReturn := fake.putStateReturnsOnCall[len(fake.putStateArgsForCall)] + fake.putStateArgsForCall = append(fake.putStateArgsForCall, struct { + arg1 string + arg2 []byte + }{arg1, arg2Copy}) + fake.recordInvocation("PutState", []interface{}{arg1, arg2Copy}) + fake.putStateMutex.Unlock() + if fake.PutStateStub != nil { + return fake.PutStateStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.putStateReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) PutStateCallCount() int { + fake.putStateMutex.RLock() + defer fake.putStateMutex.RUnlock() + return len(fake.putStateArgsForCall) +} + +func (fake *ChaincodeStub) PutStateCalls(stub func(string, []byte) error) { + fake.putStateMutex.Lock() + defer fake.putStateMutex.Unlock() + fake.PutStateStub = stub +} + +func (fake *ChaincodeStub) PutStateArgsForCall(i int) (string, []byte) { + fake.putStateMutex.RLock() + defer fake.putStateMutex.RUnlock() + argsForCall := fake.putStateArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) PutStateReturns(result1 error) { + fake.putStateMutex.Lock() + defer fake.putStateMutex.Unlock() + fake.PutStateStub = nil + fake.putStateReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) PutStateReturnsOnCall(i int, result1 error) { + fake.putStateMutex.Lock() + defer fake.putStateMutex.Unlock() + fake.PutStateStub = nil + if fake.putStateReturnsOnCall == nil { + fake.putStateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.putStateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetEvent(arg1 string, arg2 []byte) error { + var arg2Copy []byte + if arg2 != nil { + arg2Copy = make([]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.setEventMutex.Lock() + ret, specificReturn := fake.setEventReturnsOnCall[len(fake.setEventArgsForCall)] + fake.setEventArgsForCall = append(fake.setEventArgsForCall, struct { + arg1 string + arg2 []byte + }{arg1, arg2Copy}) + fake.recordInvocation("SetEvent", []interface{}{arg1, arg2Copy}) + fake.setEventMutex.Unlock() + if fake.SetEventStub != nil { + return fake.SetEventStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setEventReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) SetEventCallCount() int { + fake.setEventMutex.RLock() + defer fake.setEventMutex.RUnlock() + return len(fake.setEventArgsForCall) +} + +func (fake *ChaincodeStub) SetEventCalls(stub func(string, []byte) error) { + fake.setEventMutex.Lock() + defer fake.setEventMutex.Unlock() + fake.SetEventStub = stub +} + +func (fake *ChaincodeStub) SetEventArgsForCall(i int) (string, []byte) { + fake.setEventMutex.RLock() + defer fake.setEventMutex.RUnlock() + argsForCall := fake.setEventArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) SetEventReturns(result1 error) { + fake.setEventMutex.Lock() + defer fake.setEventMutex.Unlock() + fake.SetEventStub = nil + fake.setEventReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetEventReturnsOnCall(i int, result1 error) { + fake.setEventMutex.Lock() + defer fake.setEventMutex.Unlock() + fake.SetEventStub = nil + if fake.setEventReturnsOnCall == nil { + fake.setEventReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setEventReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameter(arg1 string, arg2 string, arg3 []byte) error { + var arg3Copy []byte + if arg3 != nil { + arg3Copy = make([]byte, len(arg3)) + copy(arg3Copy, arg3) + } + fake.setPrivateDataValidationParameterMutex.Lock() + ret, specificReturn := fake.setPrivateDataValidationParameterReturnsOnCall[len(fake.setPrivateDataValidationParameterArgsForCall)] + fake.setPrivateDataValidationParameterArgsForCall = append(fake.setPrivateDataValidationParameterArgsForCall, struct { + arg1 string + arg2 string + arg3 []byte + }{arg1, arg2, arg3Copy}) + fake.recordInvocation("SetPrivateDataValidationParameter", []interface{}{arg1, arg2, arg3Copy}) + fake.setPrivateDataValidationParameterMutex.Unlock() + if fake.SetPrivateDataValidationParameterStub != nil { + return fake.SetPrivateDataValidationParameterStub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setPrivateDataValidationParameterReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterCallCount() int { + fake.setPrivateDataValidationParameterMutex.RLock() + defer fake.setPrivateDataValidationParameterMutex.RUnlock() + return len(fake.setPrivateDataValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterCalls(stub func(string, string, []byte) error) { + fake.setPrivateDataValidationParameterMutex.Lock() + defer fake.setPrivateDataValidationParameterMutex.Unlock() + fake.SetPrivateDataValidationParameterStub = stub +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterArgsForCall(i int) (string, string, []byte) { + fake.setPrivateDataValidationParameterMutex.RLock() + defer fake.setPrivateDataValidationParameterMutex.RUnlock() + argsForCall := fake.setPrivateDataValidationParameterArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterReturns(result1 error) { + fake.setPrivateDataValidationParameterMutex.Lock() + defer fake.setPrivateDataValidationParameterMutex.Unlock() + fake.SetPrivateDataValidationParameterStub = nil + fake.setPrivateDataValidationParameterReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetPrivateDataValidationParameterReturnsOnCall(i int, result1 error) { + fake.setPrivateDataValidationParameterMutex.Lock() + defer fake.setPrivateDataValidationParameterMutex.Unlock() + fake.SetPrivateDataValidationParameterStub = nil + if fake.setPrivateDataValidationParameterReturnsOnCall == nil { + fake.setPrivateDataValidationParameterReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setPrivateDataValidationParameterReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetStateValidationParameter(arg1 string, arg2 []byte) error { + var arg2Copy []byte + if arg2 != nil { + arg2Copy = make([]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.setStateValidationParameterMutex.Lock() + ret, specificReturn := fake.setStateValidationParameterReturnsOnCall[len(fake.setStateValidationParameterArgsForCall)] + fake.setStateValidationParameterArgsForCall = append(fake.setStateValidationParameterArgsForCall, struct { + arg1 string + arg2 []byte + }{arg1, arg2Copy}) + fake.recordInvocation("SetStateValidationParameter", []interface{}{arg1, arg2Copy}) + fake.setStateValidationParameterMutex.Unlock() + if fake.SetStateValidationParameterStub != nil { + return fake.SetStateValidationParameterStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setStateValidationParameterReturns + return fakeReturns.result1 +} + +func (fake *ChaincodeStub) SetStateValidationParameterCallCount() int { + fake.setStateValidationParameterMutex.RLock() + defer fake.setStateValidationParameterMutex.RUnlock() + return len(fake.setStateValidationParameterArgsForCall) +} + +func (fake *ChaincodeStub) SetStateValidationParameterCalls(stub func(string, []byte) error) { + fake.setStateValidationParameterMutex.Lock() + defer fake.setStateValidationParameterMutex.Unlock() + fake.SetStateValidationParameterStub = stub +} + +func (fake *ChaincodeStub) SetStateValidationParameterArgsForCall(i int) (string, []byte) { + fake.setStateValidationParameterMutex.RLock() + defer fake.setStateValidationParameterMutex.RUnlock() + argsForCall := fake.setStateValidationParameterArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChaincodeStub) SetStateValidationParameterReturns(result1 error) { + fake.setStateValidationParameterMutex.Lock() + defer fake.setStateValidationParameterMutex.Unlock() + fake.SetStateValidationParameterStub = nil + fake.setStateValidationParameterReturns = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SetStateValidationParameterReturnsOnCall(i int, result1 error) { + fake.setStateValidationParameterMutex.Lock() + defer fake.setStateValidationParameterMutex.Unlock() + fake.SetStateValidationParameterStub = nil + if fake.setStateValidationParameterReturnsOnCall == nil { + fake.setStateValidationParameterReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setStateValidationParameterReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ChaincodeStub) SplitCompositeKey(arg1 string) (string, []string, error) { + fake.splitCompositeKeyMutex.Lock() + ret, specificReturn := fake.splitCompositeKeyReturnsOnCall[len(fake.splitCompositeKeyArgsForCall)] + fake.splitCompositeKeyArgsForCall = append(fake.splitCompositeKeyArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("SplitCompositeKey", []interface{}{arg1}) + fake.splitCompositeKeyMutex.Unlock() + if fake.SplitCompositeKeyStub != nil { + return fake.SplitCompositeKeyStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.splitCompositeKeyReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ChaincodeStub) SplitCompositeKeyCallCount() int { + fake.splitCompositeKeyMutex.RLock() + defer fake.splitCompositeKeyMutex.RUnlock() + return len(fake.splitCompositeKeyArgsForCall) +} + +func (fake *ChaincodeStub) SplitCompositeKeyCalls(stub func(string) (string, []string, error)) { + fake.splitCompositeKeyMutex.Lock() + defer fake.splitCompositeKeyMutex.Unlock() + fake.SplitCompositeKeyStub = stub +} + +func (fake *ChaincodeStub) SplitCompositeKeyArgsForCall(i int) string { + fake.splitCompositeKeyMutex.RLock() + defer fake.splitCompositeKeyMutex.RUnlock() + argsForCall := fake.splitCompositeKeyArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ChaincodeStub) SplitCompositeKeyReturns(result1 string, result2 []string, result3 error) { + fake.splitCompositeKeyMutex.Lock() + defer fake.splitCompositeKeyMutex.Unlock() + fake.SplitCompositeKeyStub = nil + fake.splitCompositeKeyReturns = struct { + result1 string + result2 []string + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) SplitCompositeKeyReturnsOnCall(i int, result1 string, result2 []string, result3 error) { + fake.splitCompositeKeyMutex.Lock() + defer fake.splitCompositeKeyMutex.Unlock() + fake.SplitCompositeKeyStub = nil + if fake.splitCompositeKeyReturnsOnCall == nil { + fake.splitCompositeKeyReturnsOnCall = make(map[int]struct { + result1 string + result2 []string + result3 error + }) + } + fake.splitCompositeKeyReturnsOnCall[i] = struct { + result1 string + result2 []string + result3 error + }{result1, result2, result3} +} + +func (fake *ChaincodeStub) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createCompositeKeyMutex.RLock() + defer fake.createCompositeKeyMutex.RUnlock() + fake.delPrivateDataMutex.RLock() + defer fake.delPrivateDataMutex.RUnlock() + fake.delStateMutex.RLock() + defer fake.delStateMutex.RUnlock() + fake.getArgsMutex.RLock() + defer fake.getArgsMutex.RUnlock() + fake.getArgsSliceMutex.RLock() + defer fake.getArgsSliceMutex.RUnlock() + fake.getBindingMutex.RLock() + defer fake.getBindingMutex.RUnlock() + fake.getChannelIDMutex.RLock() + defer fake.getChannelIDMutex.RUnlock() + fake.getCreatorMutex.RLock() + defer fake.getCreatorMutex.RUnlock() + fake.getDecorationsMutex.RLock() + defer fake.getDecorationsMutex.RUnlock() + fake.getFunctionAndParametersMutex.RLock() + defer fake.getFunctionAndParametersMutex.RUnlock() + fake.getHistoryForKeyMutex.RLock() + defer fake.getHistoryForKeyMutex.RUnlock() + fake.getPrivateDataMutex.RLock() + defer fake.getPrivateDataMutex.RUnlock() + fake.getPrivateDataByPartialCompositeKeyMutex.RLock() + defer fake.getPrivateDataByPartialCompositeKeyMutex.RUnlock() + fake.getPrivateDataByRangeMutex.RLock() + defer fake.getPrivateDataByRangeMutex.RUnlock() + fake.getPrivateDataHashMutex.RLock() + defer fake.getPrivateDataHashMutex.RUnlock() + fake.getPrivateDataQueryResultMutex.RLock() + defer fake.getPrivateDataQueryResultMutex.RUnlock() + fake.getPrivateDataValidationParameterMutex.RLock() + defer fake.getPrivateDataValidationParameterMutex.RUnlock() + fake.getQueryResultMutex.RLock() + defer fake.getQueryResultMutex.RUnlock() + fake.getQueryResultWithPaginationMutex.RLock() + defer fake.getQueryResultWithPaginationMutex.RUnlock() + fake.getSignedProposalMutex.RLock() + defer fake.getSignedProposalMutex.RUnlock() + fake.getStateMutex.RLock() + defer fake.getStateMutex.RUnlock() + fake.getStateByPartialCompositeKeyMutex.RLock() + defer fake.getStateByPartialCompositeKeyMutex.RUnlock() + fake.getStateByPartialCompositeKeyWithPaginationMutex.RLock() + defer fake.getStateByPartialCompositeKeyWithPaginationMutex.RUnlock() + fake.getStateByRangeMutex.RLock() + defer fake.getStateByRangeMutex.RUnlock() + fake.getStateByRangeWithPaginationMutex.RLock() + defer fake.getStateByRangeWithPaginationMutex.RUnlock() + fake.getStateValidationParameterMutex.RLock() + defer fake.getStateValidationParameterMutex.RUnlock() + fake.getStringArgsMutex.RLock() + defer fake.getStringArgsMutex.RUnlock() + fake.getTransientMutex.RLock() + defer fake.getTransientMutex.RUnlock() + fake.getTxIDMutex.RLock() + defer fake.getTxIDMutex.RUnlock() + fake.getTxTimestampMutex.RLock() + defer fake.getTxTimestampMutex.RUnlock() + fake.invokeChaincodeMutex.RLock() + defer fake.invokeChaincodeMutex.RUnlock() + fake.putPrivateDataMutex.RLock() + defer fake.putPrivateDataMutex.RUnlock() + fake.putStateMutex.RLock() + defer fake.putStateMutex.RUnlock() + fake.setEventMutex.RLock() + defer fake.setEventMutex.RUnlock() + fake.setPrivateDataValidationParameterMutex.RLock() + defer fake.setPrivateDataValidationParameterMutex.RUnlock() + fake.setStateValidationParameterMutex.RLock() + defer fake.setStateValidationParameterMutex.RUnlock() + fake.splitCompositeKeyMutex.RLock() + defer fake.splitCompositeKeyMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *ChaincodeStub) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/asset-transfer-private-data/chaincode-go/chaincode/mocks/clientIdentity.go b/asset-transfer-private-data/chaincode-go/chaincode/mocks/clientIdentity.go new file mode 100644 index 0000000..655dfd4 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/mocks/clientIdentity.go @@ -0,0 +1,399 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "crypto/x509" + "sync" +) + +type ClientIdentity struct { + AssertAttributeValueStub func(string, string) error + assertAttributeValueMutex sync.RWMutex + assertAttributeValueArgsForCall []struct { + arg1 string + arg2 string + } + assertAttributeValueReturns struct { + result1 error + } + assertAttributeValueReturnsOnCall map[int]struct { + result1 error + } + GetAttributeValueStub func(string) (string, bool, error) + getAttributeValueMutex sync.RWMutex + getAttributeValueArgsForCall []struct { + arg1 string + } + getAttributeValueReturns struct { + result1 string + result2 bool + result3 error + } + getAttributeValueReturnsOnCall map[int]struct { + result1 string + result2 bool + result3 error + } + GetIDStub func() (string, error) + getIDMutex sync.RWMutex + getIDArgsForCall []struct { + } + getIDReturns struct { + result1 string + result2 error + } + getIDReturnsOnCall map[int]struct { + result1 string + result2 error + } + GetMSPIDStub func() (string, error) + getMSPIDMutex sync.RWMutex + getMSPIDArgsForCall []struct { + } + getMSPIDReturns struct { + result1 string + result2 error + } + getMSPIDReturnsOnCall map[int]struct { + result1 string + result2 error + } + GetX509CertificateStub func() (*x509.Certificate, error) + getX509CertificateMutex sync.RWMutex + getX509CertificateArgsForCall []struct { + } + getX509CertificateReturns struct { + result1 *x509.Certificate + result2 error + } + getX509CertificateReturnsOnCall map[int]struct { + result1 *x509.Certificate + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *ClientIdentity) AssertAttributeValue(arg1 string, arg2 string) error { + fake.assertAttributeValueMutex.Lock() + ret, specificReturn := fake.assertAttributeValueReturnsOnCall[len(fake.assertAttributeValueArgsForCall)] + fake.assertAttributeValueArgsForCall = append(fake.assertAttributeValueArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("AssertAttributeValue", []interface{}{arg1, arg2}) + fake.assertAttributeValueMutex.Unlock() + if fake.AssertAttributeValueStub != nil { + return fake.AssertAttributeValueStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.assertAttributeValueReturns + return fakeReturns.result1 +} + +func (fake *ClientIdentity) AssertAttributeValueCallCount() int { + fake.assertAttributeValueMutex.RLock() + defer fake.assertAttributeValueMutex.RUnlock() + return len(fake.assertAttributeValueArgsForCall) +} + +func (fake *ClientIdentity) AssertAttributeValueCalls(stub func(string, string) error) { + fake.assertAttributeValueMutex.Lock() + defer fake.assertAttributeValueMutex.Unlock() + fake.AssertAttributeValueStub = stub +} + +func (fake *ClientIdentity) AssertAttributeValueArgsForCall(i int) (string, string) { + fake.assertAttributeValueMutex.RLock() + defer fake.assertAttributeValueMutex.RUnlock() + argsForCall := fake.assertAttributeValueArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ClientIdentity) AssertAttributeValueReturns(result1 error) { + fake.assertAttributeValueMutex.Lock() + defer fake.assertAttributeValueMutex.Unlock() + fake.AssertAttributeValueStub = nil + fake.assertAttributeValueReturns = struct { + result1 error + }{result1} +} + +func (fake *ClientIdentity) AssertAttributeValueReturnsOnCall(i int, result1 error) { + fake.assertAttributeValueMutex.Lock() + defer fake.assertAttributeValueMutex.Unlock() + fake.AssertAttributeValueStub = nil + if fake.assertAttributeValueReturnsOnCall == nil { + fake.assertAttributeValueReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.assertAttributeValueReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *ClientIdentity) GetAttributeValue(arg1 string) (string, bool, error) { + fake.getAttributeValueMutex.Lock() + ret, specificReturn := fake.getAttributeValueReturnsOnCall[len(fake.getAttributeValueArgsForCall)] + fake.getAttributeValueArgsForCall = append(fake.getAttributeValueArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetAttributeValue", []interface{}{arg1}) + fake.getAttributeValueMutex.Unlock() + if fake.GetAttributeValueStub != nil { + return fake.GetAttributeValueStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getAttributeValueReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *ClientIdentity) GetAttributeValueCallCount() int { + fake.getAttributeValueMutex.RLock() + defer fake.getAttributeValueMutex.RUnlock() + return len(fake.getAttributeValueArgsForCall) +} + +func (fake *ClientIdentity) GetAttributeValueCalls(stub func(string) (string, bool, error)) { + fake.getAttributeValueMutex.Lock() + defer fake.getAttributeValueMutex.Unlock() + fake.GetAttributeValueStub = stub +} + +func (fake *ClientIdentity) GetAttributeValueArgsForCall(i int) string { + fake.getAttributeValueMutex.RLock() + defer fake.getAttributeValueMutex.RUnlock() + argsForCall := fake.getAttributeValueArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *ClientIdentity) GetAttributeValueReturns(result1 string, result2 bool, result3 error) { + fake.getAttributeValueMutex.Lock() + defer fake.getAttributeValueMutex.Unlock() + fake.GetAttributeValueStub = nil + fake.getAttributeValueReturns = struct { + result1 string + result2 bool + result3 error + }{result1, result2, result3} +} + +func (fake *ClientIdentity) GetAttributeValueReturnsOnCall(i int, result1 string, result2 bool, result3 error) { + fake.getAttributeValueMutex.Lock() + defer fake.getAttributeValueMutex.Unlock() + fake.GetAttributeValueStub = nil + if fake.getAttributeValueReturnsOnCall == nil { + fake.getAttributeValueReturnsOnCall = make(map[int]struct { + result1 string + result2 bool + result3 error + }) + } + fake.getAttributeValueReturnsOnCall[i] = struct { + result1 string + result2 bool + result3 error + }{result1, result2, result3} +} + +func (fake *ClientIdentity) GetID() (string, error) { + fake.getIDMutex.Lock() + ret, specificReturn := fake.getIDReturnsOnCall[len(fake.getIDArgsForCall)] + fake.getIDArgsForCall = append(fake.getIDArgsForCall, struct { + }{}) + fake.recordInvocation("GetID", []interface{}{}) + fake.getIDMutex.Unlock() + if fake.GetIDStub != nil { + return fake.GetIDStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getIDReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ClientIdentity) GetIDCallCount() int { + fake.getIDMutex.RLock() + defer fake.getIDMutex.RUnlock() + return len(fake.getIDArgsForCall) +} + +func (fake *ClientIdentity) GetIDCalls(stub func() (string, error)) { + fake.getIDMutex.Lock() + defer fake.getIDMutex.Unlock() + fake.GetIDStub = stub +} + +func (fake *ClientIdentity) GetIDReturns(result1 string, result2 error) { + fake.getIDMutex.Lock() + defer fake.getIDMutex.Unlock() + fake.GetIDStub = nil + fake.getIDReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ClientIdentity) GetIDReturnsOnCall(i int, result1 string, result2 error) { + fake.getIDMutex.Lock() + defer fake.getIDMutex.Unlock() + fake.GetIDStub = nil + if fake.getIDReturnsOnCall == nil { + fake.getIDReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getIDReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ClientIdentity) GetMSPID() (string, error) { + fake.getMSPIDMutex.Lock() + ret, specificReturn := fake.getMSPIDReturnsOnCall[len(fake.getMSPIDArgsForCall)] + fake.getMSPIDArgsForCall = append(fake.getMSPIDArgsForCall, struct { + }{}) + fake.recordInvocation("GetMSPID", []interface{}{}) + fake.getMSPIDMutex.Unlock() + if fake.GetMSPIDStub != nil { + return fake.GetMSPIDStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getMSPIDReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ClientIdentity) GetMSPIDCallCount() int { + fake.getMSPIDMutex.RLock() + defer fake.getMSPIDMutex.RUnlock() + return len(fake.getMSPIDArgsForCall) +} + +func (fake *ClientIdentity) GetMSPIDCalls(stub func() (string, error)) { + fake.getMSPIDMutex.Lock() + defer fake.getMSPIDMutex.Unlock() + fake.GetMSPIDStub = stub +} + +func (fake *ClientIdentity) GetMSPIDReturns(result1 string, result2 error) { + fake.getMSPIDMutex.Lock() + defer fake.getMSPIDMutex.Unlock() + fake.GetMSPIDStub = nil + fake.getMSPIDReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ClientIdentity) GetMSPIDReturnsOnCall(i int, result1 string, result2 error) { + fake.getMSPIDMutex.Lock() + defer fake.getMSPIDMutex.Unlock() + fake.GetMSPIDStub = nil + if fake.getMSPIDReturnsOnCall == nil { + fake.getMSPIDReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getMSPIDReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *ClientIdentity) GetX509Certificate() (*x509.Certificate, error) { + fake.getX509CertificateMutex.Lock() + ret, specificReturn := fake.getX509CertificateReturnsOnCall[len(fake.getX509CertificateArgsForCall)] + fake.getX509CertificateArgsForCall = append(fake.getX509CertificateArgsForCall, struct { + }{}) + fake.recordInvocation("GetX509Certificate", []interface{}{}) + fake.getX509CertificateMutex.Unlock() + if fake.GetX509CertificateStub != nil { + return fake.GetX509CertificateStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getX509CertificateReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ClientIdentity) GetX509CertificateCallCount() int { + fake.getX509CertificateMutex.RLock() + defer fake.getX509CertificateMutex.RUnlock() + return len(fake.getX509CertificateArgsForCall) +} + +func (fake *ClientIdentity) GetX509CertificateCalls(stub func() (*x509.Certificate, error)) { + fake.getX509CertificateMutex.Lock() + defer fake.getX509CertificateMutex.Unlock() + fake.GetX509CertificateStub = stub +} + +func (fake *ClientIdentity) GetX509CertificateReturns(result1 *x509.Certificate, result2 error) { + fake.getX509CertificateMutex.Lock() + defer fake.getX509CertificateMutex.Unlock() + fake.GetX509CertificateStub = nil + fake.getX509CertificateReturns = struct { + result1 *x509.Certificate + result2 error + }{result1, result2} +} + +func (fake *ClientIdentity) GetX509CertificateReturnsOnCall(i int, result1 *x509.Certificate, result2 error) { + fake.getX509CertificateMutex.Lock() + defer fake.getX509CertificateMutex.Unlock() + fake.GetX509CertificateStub = nil + if fake.getX509CertificateReturnsOnCall == nil { + fake.getX509CertificateReturnsOnCall = make(map[int]struct { + result1 *x509.Certificate + result2 error + }) + } + fake.getX509CertificateReturnsOnCall[i] = struct { + result1 *x509.Certificate + result2 error + }{result1, result2} +} + +func (fake *ClientIdentity) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.assertAttributeValueMutex.RLock() + defer fake.assertAttributeValueMutex.RUnlock() + fake.getAttributeValueMutex.RLock() + defer fake.getAttributeValueMutex.RUnlock() + fake.getIDMutex.RLock() + defer fake.getIDMutex.RUnlock() + fake.getMSPIDMutex.RLock() + defer fake.getMSPIDMutex.RUnlock() + fake.getX509CertificateMutex.RLock() + defer fake.getX509CertificateMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *ClientIdentity) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/asset-transfer-private-data/chaincode-go/chaincode/mocks/statequeryiterator.go b/asset-transfer-private-data/chaincode-go/chaincode/mocks/statequeryiterator.go new file mode 100644 index 0000000..27e3034 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/mocks/statequeryiterator.go @@ -0,0 +1,232 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/hyperledger/fabric-protos-go/ledger/queryresult" +) + +type StateQueryIterator struct { + CloseStub func() error + closeMutex sync.RWMutex + closeArgsForCall []struct { + } + closeReturns struct { + result1 error + } + closeReturnsOnCall map[int]struct { + result1 error + } + HasNextStub func() bool + hasNextMutex sync.RWMutex + hasNextArgsForCall []struct { + } + hasNextReturns struct { + result1 bool + } + hasNextReturnsOnCall map[int]struct { + result1 bool + } + NextStub func() (*queryresult.KV, error) + nextMutex sync.RWMutex + nextArgsForCall []struct { + } + nextReturns struct { + result1 *queryresult.KV + result2 error + } + nextReturnsOnCall map[int]struct { + result1 *queryresult.KV + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *StateQueryIterator) Close() error { + fake.closeMutex.Lock() + ret, specificReturn := fake.closeReturnsOnCall[len(fake.closeArgsForCall)] + fake.closeArgsForCall = append(fake.closeArgsForCall, struct { + }{}) + fake.recordInvocation("Close", []interface{}{}) + fake.closeMutex.Unlock() + if fake.CloseStub != nil { + return fake.CloseStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.closeReturns + return fakeReturns.result1 +} + +func (fake *StateQueryIterator) CloseCallCount() int { + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + return len(fake.closeArgsForCall) +} + +func (fake *StateQueryIterator) CloseCalls(stub func() error) { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.CloseStub = stub +} + +func (fake *StateQueryIterator) CloseReturns(result1 error) { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.CloseStub = nil + fake.closeReturns = struct { + result1 error + }{result1} +} + +func (fake *StateQueryIterator) CloseReturnsOnCall(i int, result1 error) { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.CloseStub = nil + if fake.closeReturnsOnCall == nil { + fake.closeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.closeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *StateQueryIterator) HasNext() bool { + fake.hasNextMutex.Lock() + ret, specificReturn := fake.hasNextReturnsOnCall[len(fake.hasNextArgsForCall)] + fake.hasNextArgsForCall = append(fake.hasNextArgsForCall, struct { + }{}) + fake.recordInvocation("HasNext", []interface{}{}) + fake.hasNextMutex.Unlock() + if fake.HasNextStub != nil { + return fake.HasNextStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.hasNextReturns + return fakeReturns.result1 +} + +func (fake *StateQueryIterator) HasNextCallCount() int { + fake.hasNextMutex.RLock() + defer fake.hasNextMutex.RUnlock() + return len(fake.hasNextArgsForCall) +} + +func (fake *StateQueryIterator) HasNextCalls(stub func() bool) { + fake.hasNextMutex.Lock() + defer fake.hasNextMutex.Unlock() + fake.HasNextStub = stub +} + +func (fake *StateQueryIterator) HasNextReturns(result1 bool) { + fake.hasNextMutex.Lock() + defer fake.hasNextMutex.Unlock() + fake.HasNextStub = nil + fake.hasNextReturns = struct { + result1 bool + }{result1} +} + +func (fake *StateQueryIterator) HasNextReturnsOnCall(i int, result1 bool) { + fake.hasNextMutex.Lock() + defer fake.hasNextMutex.Unlock() + fake.HasNextStub = nil + if fake.hasNextReturnsOnCall == nil { + fake.hasNextReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.hasNextReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *StateQueryIterator) Next() (*queryresult.KV, error) { + fake.nextMutex.Lock() + ret, specificReturn := fake.nextReturnsOnCall[len(fake.nextArgsForCall)] + fake.nextArgsForCall = append(fake.nextArgsForCall, struct { + }{}) + fake.recordInvocation("Next", []interface{}{}) + fake.nextMutex.Unlock() + if fake.NextStub != nil { + return fake.NextStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.nextReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *StateQueryIterator) NextCallCount() int { + fake.nextMutex.RLock() + defer fake.nextMutex.RUnlock() + return len(fake.nextArgsForCall) +} + +func (fake *StateQueryIterator) NextCalls(stub func() (*queryresult.KV, error)) { + fake.nextMutex.Lock() + defer fake.nextMutex.Unlock() + fake.NextStub = stub +} + +func (fake *StateQueryIterator) NextReturns(result1 *queryresult.KV, result2 error) { + fake.nextMutex.Lock() + defer fake.nextMutex.Unlock() + fake.NextStub = nil + fake.nextReturns = struct { + result1 *queryresult.KV + result2 error + }{result1, result2} +} + +func (fake *StateQueryIterator) NextReturnsOnCall(i int, result1 *queryresult.KV, result2 error) { + fake.nextMutex.Lock() + defer fake.nextMutex.Unlock() + fake.NextStub = nil + if fake.nextReturnsOnCall == nil { + fake.nextReturnsOnCall = make(map[int]struct { + result1 *queryresult.KV + result2 error + }) + } + fake.nextReturnsOnCall[i] = struct { + result1 *queryresult.KV + result2 error + }{result1, result2} +} + +func (fake *StateQueryIterator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + fake.hasNextMutex.RLock() + defer fake.hasNextMutex.RUnlock() + fake.nextMutex.RLock() + defer fake.nextMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *StateQueryIterator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/asset-transfer-private-data/chaincode-go/chaincode/mocks/transaction.go b/asset-transfer-private-data/chaincode-go/chaincode/mocks/transaction.go new file mode 100644 index 0000000..eea37db --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/chaincode/mocks/transaction.go @@ -0,0 +1,164 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/hyperledger/fabric-chaincode-go/pkg/cid" + "github.com/hyperledger/fabric-chaincode-go/shim" +) + +type TransactionContext struct { + GetClientIdentityStub func() cid.ClientIdentity + getClientIdentityMutex sync.RWMutex + getClientIdentityArgsForCall []struct { + } + getClientIdentityReturns struct { + result1 cid.ClientIdentity + } + getClientIdentityReturnsOnCall map[int]struct { + result1 cid.ClientIdentity + } + GetStubStub func() shim.ChaincodeStubInterface + getStubMutex sync.RWMutex + getStubArgsForCall []struct { + } + getStubReturns struct { + result1 shim.ChaincodeStubInterface + } + getStubReturnsOnCall map[int]struct { + result1 shim.ChaincodeStubInterface + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *TransactionContext) GetClientIdentity() cid.ClientIdentity { + fake.getClientIdentityMutex.Lock() + ret, specificReturn := fake.getClientIdentityReturnsOnCall[len(fake.getClientIdentityArgsForCall)] + fake.getClientIdentityArgsForCall = append(fake.getClientIdentityArgsForCall, struct { + }{}) + fake.recordInvocation("GetClientIdentity", []interface{}{}) + fake.getClientIdentityMutex.Unlock() + if fake.GetClientIdentityStub != nil { + return fake.GetClientIdentityStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getClientIdentityReturns + return fakeReturns.result1 +} + +func (fake *TransactionContext) GetClientIdentityCallCount() int { + fake.getClientIdentityMutex.RLock() + defer fake.getClientIdentityMutex.RUnlock() + return len(fake.getClientIdentityArgsForCall) +} + +func (fake *TransactionContext) GetClientIdentityCalls(stub func() cid.ClientIdentity) { + fake.getClientIdentityMutex.Lock() + defer fake.getClientIdentityMutex.Unlock() + fake.GetClientIdentityStub = stub +} + +func (fake *TransactionContext) GetClientIdentityReturns(result1 cid.ClientIdentity) { + fake.getClientIdentityMutex.Lock() + defer fake.getClientIdentityMutex.Unlock() + fake.GetClientIdentityStub = nil + fake.getClientIdentityReturns = struct { + result1 cid.ClientIdentity + }{result1} +} + +func (fake *TransactionContext) GetClientIdentityReturnsOnCall(i int, result1 cid.ClientIdentity) { + fake.getClientIdentityMutex.Lock() + defer fake.getClientIdentityMutex.Unlock() + fake.GetClientIdentityStub = nil + if fake.getClientIdentityReturnsOnCall == nil { + fake.getClientIdentityReturnsOnCall = make(map[int]struct { + result1 cid.ClientIdentity + }) + } + fake.getClientIdentityReturnsOnCall[i] = struct { + result1 cid.ClientIdentity + }{result1} +} + +func (fake *TransactionContext) GetStub() shim.ChaincodeStubInterface { + fake.getStubMutex.Lock() + ret, specificReturn := fake.getStubReturnsOnCall[len(fake.getStubArgsForCall)] + fake.getStubArgsForCall = append(fake.getStubArgsForCall, struct { + }{}) + fake.recordInvocation("GetStub", []interface{}{}) + fake.getStubMutex.Unlock() + if fake.GetStubStub != nil { + return fake.GetStubStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.getStubReturns + return fakeReturns.result1 +} + +func (fake *TransactionContext) GetStubCallCount() int { + fake.getStubMutex.RLock() + defer fake.getStubMutex.RUnlock() + return len(fake.getStubArgsForCall) +} + +func (fake *TransactionContext) GetStubCalls(stub func() shim.ChaincodeStubInterface) { + fake.getStubMutex.Lock() + defer fake.getStubMutex.Unlock() + fake.GetStubStub = stub +} + +func (fake *TransactionContext) GetStubReturns(result1 shim.ChaincodeStubInterface) { + fake.getStubMutex.Lock() + defer fake.getStubMutex.Unlock() + fake.GetStubStub = nil + fake.getStubReturns = struct { + result1 shim.ChaincodeStubInterface + }{result1} +} + +func (fake *TransactionContext) GetStubReturnsOnCall(i int, result1 shim.ChaincodeStubInterface) { + fake.getStubMutex.Lock() + defer fake.getStubMutex.Unlock() + fake.GetStubStub = nil + if fake.getStubReturnsOnCall == nil { + fake.getStubReturnsOnCall = make(map[int]struct { + result1 shim.ChaincodeStubInterface + }) + } + fake.getStubReturnsOnCall[i] = struct { + result1 shim.ChaincodeStubInterface + }{result1} +} + +func (fake *TransactionContext) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getClientIdentityMutex.RLock() + defer fake.getClientIdentityMutex.RUnlock() + fake.getStubMutex.RLock() + defer fake.getStubMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *TransactionContext) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/asset-transfer-private-data/chaincode-go/collections_config.json b/asset-transfer-private-data/chaincode-go/collections_config.json new file mode 100644 index 0000000..cb3729a --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/collections_config.json @@ -0,0 +1,35 @@ +[ + { + "name": "assetCollection", + "policy": "OR('Org1MSP.member', 'Org2MSP.member')", + "requiredPeerCount": 1, + "maxPeerCount": 1, + "blockToLive":1000000, + "memberOnlyRead": true, + "memberOnlyWrite": true +}, + { + "name": "Org1MSPPrivateCollection", + "policy": "OR('Org1MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org1MSP.member')" + } + }, + { + "name": "Org2MSPPrivateCollection", + "policy": "OR('Org2MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org2MSP.member')" + } + } +] diff --git a/asset-transfer-private-data/chaincode-go/go.mod b/asset-transfer-private-data/chaincode-go/go.mod new file mode 100644 index 0000000..a4725c4 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/go.mod @@ -0,0 +1,25 @@ +module github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go + +go 1.14 + +require ( + github.com/go-openapi/jsonreference v0.19.4 // indirect + github.com/go-openapi/spec v0.19.8 // indirect + github.com/go-openapi/swag v0.19.9 // indirect + github.com/gobuffalo/envy v1.9.0 // indirect + github.com/gobuffalo/packd v1.0.0 // indirect + github.com/golang/protobuf v1.4.2 // indirect + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200511190512-bcfeb58dd83a + github.com/hyperledger/fabric-contract-api-go v1.1.0 + github.com/hyperledger/fabric-protos-go v0.0.0-20200707132912-fee30f3ccd23 + github.com/mailru/easyjson v0.7.1 // indirect + github.com/rogpeppe/go-internal v1.6.0 // indirect + github.com/stretchr/testify v1.5.1 + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect + golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 // indirect + google.golang.org/genproto v0.0.0-20200721032028-5044d0edf986 // indirect + google.golang.org/grpc v1.30.0 // indirect + google.golang.org/protobuf v1.25.0 + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/asset-transfer-private-data/chaincode-go/go.sum b/asset-transfer-private-data/chaincode-go/go.sum new file mode 100644 index 0000000..f22cc6e --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/go.sum @@ -0,0 +1,234 @@ +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/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/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/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg= +github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +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/spec v0.19.8 h1:qAdZLh1r6QF/hI/gTq+TJTvsQUodZsM7KLqkAJdiJNg= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +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-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +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/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE= +github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +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/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= +github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= +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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +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/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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-chaincode-go v0.0.0-20200511190512-bcfeb58dd83a h1:KoFw2HnRfW+EItMP0zvUUl1FGzDb/7O0ov7uXZffQok= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200511190512-bcfeb58dd83a/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/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/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/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +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/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.0 h1:IZRgg4sfrDH7nsAD1Y/Nwj+GzIfEwpJSLjCaNC3SbsI= +github.com/rogpeppe/go-internal v1.6.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +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/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/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/crypto v0.0.0-20200622213623-75b288015ac9/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/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-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-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/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +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-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-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/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 h1:gVCS+QOncANNPlmlO1AhlU3oxs4V9z+gTtPwIk3p2N8= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/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/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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-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= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200721032028-5044d0edf986 h1:10ohwcLf82I55O/aQxYqmWKoOdNbQTYYComeP1KDOS4= +google.golang.org/genproto v0.0.0-20200721032028-5044d0edf986/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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.4/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= +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= diff --git a/asset-transfer-private-data/chaincode-go/main.go b/asset-transfer-private-data/chaincode-go/main.go new file mode 100644 index 0000000..6e0d671 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/main.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/asset-transfer-private-data/chaincode-go/chaincode" +) + +func main() { + assetChaincode, err := contractapi.NewChaincode(&chaincode.SmartContract{}) + if err != nil { + log.Panicf("Error creating asset-transfer-private-data chaincode: %v", err) + } + + if err := assetChaincode.Start(); err != nil { + log.Panicf("Error starting asset-transfer-private-data chaincode: %v", err) + } +} diff --git a/asset-transfer-private-data/chaincode-java/.gitattributes b/asset-transfer-private-data/chaincode-java/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/asset-transfer-private-data/chaincode-java/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json b/asset-transfer-private-data/chaincode-java/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json new file mode 100644 index 0000000..2e2a5c6 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json @@ -0,0 +1,11 @@ +{ + "index": { + "fields": [ + "objectType", + "owner" + ] + }, + "ddoc": "indexOwnerDoc", + "name": "indexOwner", + "type": "json" +} diff --git a/asset-transfer-private-data/chaincode-java/build.gradle b/asset-transfer-private-data/chaincode-java/build.gradle new file mode 100644 index 0000000..bb48d5b --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/build.gradle @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'application' + id 'checkstyle' + id 'jacoco' +} + +group 'org.hyperledger.fabric.samples' +version '1.0-SNAPSHOT' + +dependencies { + + compileOnly 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + + testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +repositories { + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + jcenter() + maven { + url 'https://jitpack.io' + } +} + +application { + mainClass = 'org.hyperledger.fabric.contract.ContractRouter' +} + +checkstyle { + toolVersion '8.21' + configFile file("config/checkstyle/checkstyle.xml") +} + +checkstyleMain { + source ='src/main/java' +} + +checkstyleTest { + source ='src/test/java' +} + +jacocoTestReport { + dependsOn test +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +installDist.dependsOn check \ No newline at end of file diff --git a/asset-transfer-private-data/chaincode-java/collections_config.json b/asset-transfer-private-data/chaincode-java/collections_config.json new file mode 100644 index 0000000..cb3729a --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/collections_config.json @@ -0,0 +1,35 @@ +[ + { + "name": "assetCollection", + "policy": "OR('Org1MSP.member', 'Org2MSP.member')", + "requiredPeerCount": 1, + "maxPeerCount": 1, + "blockToLive":1000000, + "memberOnlyRead": true, + "memberOnlyWrite": true +}, + { + "name": "Org1MSPPrivateCollection", + "policy": "OR('Org1MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org1MSP.member')" + } + }, + { + "name": "Org2MSPPrivateCollection", + "policy": "OR('Org2MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org2MSP.member')" + } + } +] diff --git a/asset-transfer-private-data/chaincode-java/config/checkstyle/checkstyle.xml b/asset-transfer-private-data/chaincode-java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..acd5df4 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/config/checkstyle/checkstyle.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/asset-transfer-private-data/chaincode-java/config/checkstyle/suppressions.xml b/asset-transfer-private-data/chaincode-java/config/checkstyle/suppressions.xml new file mode 100644 index 0000000..33dda04 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/config/checkstyle/suppressions.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-private-data/chaincode-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/asset-transfer-private-data/chaincode-java/gradlew.bat b/asset-transfer-private-data/chaincode-java/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/asset-transfer-private-data/chaincode-java/settings.gradle b/asset-transfer-private-data/chaincode-java/settings.gradle new file mode 100644 index 0000000..59cbbbc --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/settings.gradle @@ -0,0 +1,5 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'private' diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/Asset.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/Asset.java new file mode 100644 index 0000000..11b2910 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/Asset.java @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.privatedata; + +import java.util.Objects; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +import org.hyperledger.fabric.shim.ChaincodeException; +import org.json.JSONObject; + +@DataType() +public final class Asset { + + @Property() + private final String assetID; + + @Property() + private final String objectType; + + @Property() + private final String color; + + @Property() + private final int size; + + @Property() + private String owner; + + public String getAssetID() { + return assetID; + } + + public String getColor() { + return color; + } + + public int getSize() { + return size; + } + + public String getOwner() { + return owner; + } + + public String getObjectType() { + return objectType; + } + + public void setOwner(final String newowner) { + owner = newowner; + } + + public Asset(final String type, + final String assetID, final String color, + final int size, final String owner) { + this.objectType = type; + this.assetID = assetID; + this.color = color; + this.size = size; + this.owner = owner; + } + + public byte[] serialize() { + String jsonStr = new JSONObject(this).toString(); + return jsonStr.getBytes(UTF_8); + } + + public static Asset deserialize(final byte[] assetJSON) { + return deserialize(new String(assetJSON, UTF_8)); + } + + public static Asset deserialize(final String assetJSON) { + try { + JSONObject json = new JSONObject(assetJSON); + final String id = json.getString("assetID"); + final String type = json.getString("objectType"); + final String color = json.getString("color"); + final String owner = json.getString("owner"); + final int size = json.getInt("size"); + return new Asset(type, id, color, size, owner); + } catch (Exception e) { + throw new ChaincodeException("Deserialize error: " + e.getMessage(), "DATA_ERROR"); + } + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + Asset other = (Asset) obj; + + return Objects.deepEquals( + new String[]{getAssetID(), getColor(), getOwner()}, + new String[]{other.getAssetID(), other.getColor(), other.getOwner()}) + && + Objects.deepEquals( + new int[]{getSize()}, + new int[]{other.getSize()}); + } + + @Override + public int hashCode() { + return Objects.hash(getObjectType(), getAssetID(), getColor(), getSize(), getOwner()); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + + " [assetID=" + assetID + ", type=" + objectType + ", color=" + + color + ", size=" + size + ", owner=" + owner + "]"; + } + + +} diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetPrivateDetails.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetPrivateDetails.java new file mode 100644 index 0000000..da1c6e2 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetPrivateDetails.java @@ -0,0 +1,51 @@ +package org.hyperledger.fabric.samples.privatedata; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +import org.hyperledger.fabric.shim.ChaincodeException; +import org.json.JSONObject; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@DataType() +public final class AssetPrivateDetails { + + @Property() + private final String assetID; + + @Property() + private int appraisedValue; + + public String getAssetID() { + return assetID; + } + + public int getAppraisedValue() { + return appraisedValue; + } + + public AssetPrivateDetails(final String assetID, + final int appraisedValue) { + this.assetID = assetID; + this.appraisedValue = appraisedValue; + } + + public byte[] serialize() { + String jsonStr = new JSONObject(this).toString(); + return jsonStr.getBytes(UTF_8); + } + + public static AssetPrivateDetails deserialize(final byte[] assetJSON) { + try { + JSONObject json = new JSONObject(new String(assetJSON, UTF_8)); + final String id = json.getString("assetID"); + final int appraisedValue = json.getInt("appraisedValue"); + return new AssetPrivateDetails(id, appraisedValue); + } catch (Exception e) { + throw new ChaincodeException("Deserialize error: " + e.getMessage(), "DATA_ERROR"); + } + } + + +} diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetTransfer.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetTransfer.java new file mode 100644 index 0000000..f95a234 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/AssetTransfer.java @@ -0,0 +1,593 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.privatedata; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contact; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.CompositeKey; + +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Main Chaincode class. A ContractInterface gets converted to Chaincode internally. + * @see org.hyperledger.fabric.shim.Chaincode + * + * Each chaincode transaction function must take, Context as first parameter. + * Unless specified otherwise via annotation (@Contract or @Transaction), the contract name + * is the class name (without package) + * and the transaction name is the method name. + * + * To create fabric test-network + * cd fabric-samples/test-network + * ./network.sh up createChannel -ca -s couchdb + * To deploy this chaincode to test-network, use the collection config as described in + * See queryResults = new ArrayList<>(); + // retrieve asset with keys between startKey (inclusive) and endKey(exclusive) in lexical order. + try (QueryResultsIterator results = stub.getPrivateDataByRange(ASSET_COLLECTION_NAME, startKey, endKey)) { + for (KeyValue result : results) { + if (result.getStringValue() == null || result.getStringValue().length() == 0) { + System.err.printf("Invalid Asset json: %s\n", result.getStringValue()); + continue; + } + Asset asset = Asset.deserialize(result.getStringValue()); + queryResults.add(asset); + System.out.println("QueryResult: " + asset.toString()); + } + } + return queryResults.toArray(new Asset[0]); + } + + // =======Rich queries ========================================================================= + // Two examples of rich queries are provided below (parameterized query and ad hoc query). + // Rich queries pass a query string to the state database. + // Rich queries are only supported by state database implementations + // that support rich query (e.g. CouchDB). + // The query string is in the syntax of the underlying state database. + // With rich queries there is no guarantee that the result set hasn't changed between + // endorsement time and commit time, aka 'phantom reads'. + // Therefore, rich queries should not be used in update transactions, unless the + // application handles the possibility of result set changes between endorsement and commit time. + // Rich queries can be used for point-in-time queries against a peer. + // ============================================================================================ + + /** + * QueryAssetByOwner queries for assets based on assetType, owner. + * This is an example of a parameterized query where the query logic is baked into the chaincode, + * and accepting a single query parameter (owner). + * + * @param ctx the transaction context + * @param assetType type to query for + * @param owner asset owner to query for + * @return the asset found on the ledger if there was one + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public Asset[] QueryAssetByOwner(final Context ctx, final String assetType, final String owner) throws Exception { + String queryString = String.format("{\"selector\":{\"objectType\":\"%s\",\"owner\":\"%s\"}}", assetType, owner); + return getQueryResult(ctx, queryString); + } + + /** + * QueryAssets uses a query string to perform a query for assets. + * Query string matching state database syntax is passed in and executed as is. + * Supports ad hoc queries that can be defined at runtime by the client. + * + * @param ctx the transaction context + * @param queryString query string matching state database syntax + * @return the asset found on the ledger if there was one + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public Asset[] QueryAssets(final Context ctx, final String queryString) throws Exception { + return getQueryResult(ctx, queryString); + } + + private Asset[] getQueryResult(final Context ctx, final String queryString) throws Exception { + ChaincodeStub stub = ctx.getStub(); + System.out.printf("QueryAssets: %s\n", queryString); + + List queryResults = new ArrayList(); + // retrieve asset with keys between startKey (inclusive) and endKey(exclusive) in lexical order. + try (QueryResultsIterator results = stub.getPrivateDataQueryResult(ASSET_COLLECTION_NAME, queryString)) { + for (KeyValue result : results) { + if (result.getStringValue() == null || result.getStringValue().length() == 0) { + System.err.printf("Invalid Asset json: %s\n", result.getStringValue()); + continue; + } + Asset asset = Asset.deserialize(result.getStringValue()); + queryResults.add(asset); + System.out.println("QueryResult: " + asset.toString()); + } + } + return queryResults.toArray(new Asset[0]); + } + + + /** + * Creates a new asset on the ledger from asset properties passed in as transient map. + * Asset owner will be inferred from the ClientId via stub api + * + * @param ctx the transaction context + * Transient map with asset_properties key with asset json as value + * @return the created asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset CreateAsset(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + Map transientMap = ctx.getStub().getTransient(); + if (!transientMap.containsKey("asset_properties")) { + String errorMessage = String.format("CreateAsset call must specify asset_properties in Transient map input"); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + byte[] transientAssetJSON = transientMap.get("asset_properties"); + final String assetID; + final String type; + final String color; + int appraisedValue = 0; + int size = 0; + try { + JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8)); + Map tMap = json.toMap(); + + type = (String) tMap.get("objectType"); + assetID = (String) tMap.get("assetID"); + color = (String) tMap.get("color"); + if (tMap.containsKey("size")) { + size = (Integer) tMap.get("size"); + } + if (tMap.containsKey("appraisedValue")) { + appraisedValue = (Integer) tMap.get("appraisedValue"); + } + } catch (Exception err) { + String errorMessage = String.format("TransientMap deserialized error: %s ", err); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + //input validations + String errorMessage = null; + if (assetID.equals("")) { + errorMessage = String.format("Empty input in Transient map: assetID"); + } + if (type.equals("")) { + errorMessage = String.format("Empty input in Transient map: objectType"); + } + if (color.equals("")) { + errorMessage = String.format("Empty input in Transient map: color"); + } + if (size <= 0) { + errorMessage = String.format("Empty input in Transient map: size"); + } + if (appraisedValue <= 0) { + errorMessage = String.format("Empty input in Transient map: appraisedValue"); + } + + if (errorMessage != null) { + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + Asset asset = new Asset(type, assetID, color, size, ""); + // Check if asset already exists + byte[] assetJSON = ctx.getStub().getPrivateData(ASSET_COLLECTION_NAME, assetID); + if (assetJSON != null && assetJSON.length > 0) { + errorMessage = String.format("Asset %s already exists", assetID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString()); + } + + // Get ID of submitting client identity + String clientID = ctx.getClientIdentity().getId(); + + // Verify that the client is submitting request to peer in their organization + // This is to ensure that a client from another org doesn't attempt to read or + // write private data from this peer. + verifyClientOrgMatchesPeerOrg(ctx); + + //Make submitting client the owner + asset.setOwner(clientID); + System.out.printf("CreateAsset Put: collection %s, ID %s\n", ASSET_COLLECTION_NAME, assetID); + System.out.printf("Put: collection %s, ID %s\n", ASSET_COLLECTION_NAME, new String(asset.serialize())); + stub.putPrivateData(ASSET_COLLECTION_NAME, assetID, asset.serialize()); + + // Get collection name for this organization. + String orgCollectionName = getCollectionName(ctx); + + //Save AssetPrivateDetails to org collection + AssetPrivateDetails assetPriv = new AssetPrivateDetails(assetID, appraisedValue); + System.out.printf("Put AssetPrivateDetails: collection %s, ID %s\n", orgCollectionName, assetID); + stub.putPrivateData(orgCollectionName, assetID, assetPriv.serialize()); + + return asset; + } + + /** + * AgreeToTransfer is used by the potential buyer of the asset to agree to the + * asset value. The agreed to appraisal value is stored in the buying orgs + * org specifc collection, while the the buyer client ID is stored in the asset collection + * using a composite key + * Uses transient map with key asset_value + * + * @param ctx the transaction context + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void AgreeToTransfer(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + Map transientMap = ctx.getStub().getTransient(); + if (!transientMap.containsKey("asset_value")) { + String errorMessage = String.format("AgreeToTransfer call must specify \"asset_value\" in Transient map input"); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + byte[] transientAssetJSON = transientMap.get("asset_value"); + AssetPrivateDetails assetPriv; + String assetID; + try { + JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8)); + assetID = json.getString("assetID"); + final int appraisedValue = json.getInt("appraisedValue"); + + assetPriv = new AssetPrivateDetails(assetID, appraisedValue); + } catch (Exception err) { + String errorMessage = String.format("TransientMap deserialized error %s ", err); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + if (assetID.equals("")) { + String errorMessage = String.format("Invalid input in Transient map: assetID"); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + if (assetPriv.getAppraisedValue() <= 0) { // appraisedValue field must be a positive integer + String errorMessage = String.format("Input must be positive integer: appraisedValue"); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + System.out.printf("AgreeToTransfer: verify asset %s exists\n", assetID); + Asset existing = ReadAsset(ctx, assetID); + if (existing == null) { + String errorMessage = String.format("Asset does not exist in the collection: ", assetID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + // Get collection name for this organization. + String orgCollectionName = getCollectionName(ctx); + + verifyClientOrgMatchesPeerOrg(ctx); + + //Save AssetPrivateDetails to org collection + System.out.printf("Put AssetPrivateDetails: collection %s, ID %s\n", orgCollectionName, assetID); + stub.putPrivateData(orgCollectionName, assetID, assetPriv.serialize()); + + String clientID = ctx.getClientIdentity().getId(); + //Write the AgreeToTransfer key in assetCollection + CompositeKey aggKey = stub.createCompositeKey(AGREEMENT_KEYPREFIX, assetID); + System.out.printf("AgreeToTransfer Put: collection %s, ID %s, Key %s\n", ASSET_COLLECTION_NAME, assetID, aggKey); + stub.putPrivateData(ASSET_COLLECTION_NAME, aggKey.toString(), clientID); + } + + /** + * TransferAsset transfers the asset to the new owner by setting a new owner ID based on + * AgreeToTransfer data + * + * @param ctx the transaction context + * @return none + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void TransferAsset(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + Map transientMap = ctx.getStub().getTransient(); + if (!transientMap.containsKey("asset_owner")) { + String errorMessage = "TransferAsset call must specify \"asset_owner\" in Transient map input"; + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + byte[] transientAssetJSON = transientMap.get("asset_owner"); + final String assetID; + final String buyerMSP; + try { + JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8)); + assetID = json.getString("assetID"); + buyerMSP = json.getString("buyerMSP"); + + } catch (Exception err) { + String errorMessage = String.format("TransientMap deserialized error %s ", err); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + if (assetID.equals("")) { + String errorMessage = String.format("Invalid input in Transient map: " + "assetID"); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + if (buyerMSP.equals("")) { + String errorMessage = String.format("Invalid input in Transient map: " + "buyerMSP"); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + System.out.printf("TransferAsset: verify asset %s exists\n", assetID); + byte[] assetJSON = stub.getPrivateData(ASSET_COLLECTION_NAME, assetID); + + if (assetJSON == null || assetJSON.length == 0) { + String errorMessage = String.format("Asset %s does not exist in the collection", assetID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + verifyClientOrgMatchesPeerOrg(ctx); + Asset thisAsset = Asset.deserialize(assetJSON); + // Verify transfer details and transfer owner + verifyAgreement(ctx, assetID, thisAsset.getOwner(), buyerMSP); + + TransferAgreement transferAgreement = ReadTransferAgreement(ctx, assetID); + if (transferAgreement == null) { + String errorMessage = String.format("TransferAgreement does not exist for asset: %s", assetID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + // Transfer asset in private data collection to new owner + String newOwner = transferAgreement.getBuyerID(); + thisAsset.setOwner(newOwner); + + //Save updated Asset to collection + System.out.printf("Transfer Asset: collection %s, ID %s to owner %s\n", ASSET_COLLECTION_NAME, assetID, newOwner); + stub.putPrivateData(ASSET_COLLECTION_NAME, assetID, thisAsset.serialize()); + + // delete the key from owners collection + String ownersCollectionName = getCollectionName(ctx); + stub.delPrivateData(ownersCollectionName, assetID); + + //Delete the transfer agreement from the asset collection + CompositeKey aggKey = stub.createCompositeKey(AGREEMENT_KEYPREFIX, assetID); + System.out.printf("AgreeToTransfer deleteKey: collection %s, ID %s, Key %s\n", ASSET_COLLECTION_NAME, assetID, aggKey); + stub.delPrivateData(ASSET_COLLECTION_NAME, aggKey.toString()); + } + + /** + * Deletes a asset & related details from the ledger. + * Input in transient map: asset_delete + * + * @param ctx the transaction context + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void DeleteAsset(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + Map transientMap = ctx.getStub().getTransient(); + if (!transientMap.containsKey("asset_delete")) { + String errorMessage = String.format("DeleteAsset call must specify 'asset_delete' in Transient map input"); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + byte[] transientAssetJSON = transientMap.get("asset_delete"); + final String assetID; + + try { + JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8)); + assetID = json.getString("assetID"); + + } catch (Exception err) { + String errorMessage = String.format("TransientMap deserialized error: %s ", err); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString()); + } + + System.out.printf("DeleteAsset: verify asset %s exists\n", assetID); + byte[] assetJSON = stub.getPrivateData(ASSET_COLLECTION_NAME, assetID); + + if (assetJSON == null || assetJSON.length == 0) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + String ownersCollectionName = getCollectionName(ctx); + byte[] apdJSON = stub.getPrivateData(ownersCollectionName, assetID); + + if (apdJSON == null || apdJSON.length == 0) { + String errorMessage = String.format("Failed to read asset from owner's Collection %s", ownersCollectionName); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + verifyClientOrgMatchesPeerOrg(ctx); + + // delete the key from asset collection + System.out.printf("DeleteAsset: collection %s, ID %s\n", ASSET_COLLECTION_NAME, assetID); + stub.delPrivateData(ASSET_COLLECTION_NAME, assetID); + + // Finally, delete private details of asset + stub.delPrivateData(ownersCollectionName, assetID); + } + + // Used by TransferAsset to verify that the transfer is being initiated by the owner and that + // the buyer has agreed to the same appraisal value as the owner + private void verifyAgreement(final Context ctx, final String assetID, final String owner, final String buyerMSP) { + String clienID = ctx.getClientIdentity().getId(); + + // Check 1: verify that the transfer is being initiatied by the owner + if (!clienID.equals(owner)) { + throw new ChaincodeException("Submitting client identity does not own the asset", AssetTransferErrors.INVALID_ACCESS.toString()); + } + + // Check 2: verify that the buyer has agreed to the appraised value + String collectionOwner = getCollectionName(ctx); // get owner collection from caller identity + String collectionBuyer = buyerMSP + "PrivateCollection"; + + // Get hash of owners agreed to value + byte[] ownerAppraisedValueHash = ctx.getStub().getPrivateDataHash(collectionOwner, assetID); + if (ownerAppraisedValueHash == null) { + throw new ChaincodeException(String.format("Hash of appraised value for %s does not exist in collection %s", assetID, collectionOwner)); + } + + // Get hash of buyers agreed to value + byte[] buyerAppraisedValueHash = ctx.getStub().getPrivateDataHash(collectionBuyer, assetID); + if (buyerAppraisedValueHash == null) { + throw new ChaincodeException(String.format("Hash of appraised value for %s does not exist in collection %s. AgreeToTransfer must be called by the buyer first.", assetID, collectionBuyer)); + } + + // Verify that the two hashes match + if (!Arrays.equals(ownerAppraisedValueHash, buyerAppraisedValueHash)) { + throw new ChaincodeException(String.format("Hash for appraised value for owner %x does not match value for seller %x", ownerAppraisedValueHash, buyerAppraisedValueHash)); + } + } + + private void verifyClientOrgMatchesPeerOrg(final Context ctx) { + String clientMSPID = ctx.getClientIdentity().getMSPID(); + String peerMSPID = ctx.getStub().getMspId(); + + if (!peerMSPID.equals(clientMSPID)) { + String errorMessage = String.format("Client from org %s is not authorized to read or write private data from an org %s peer", clientMSPID, peerMSPID); + System.err.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.INVALID_ACCESS.toString()); + } + } + + private String getCollectionName(final Context ctx) { + + // Get the MSP ID of submitting client identity + String clientMSPID = ctx.getClientIdentity().getMSPID(); + // Create the collection name + return clientMSPID + "PrivateCollection"; + } + +} diff --git a/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/TransferAgreement.java b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/TransferAgreement.java new file mode 100644 index 0000000..2082d97 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/src/main/java/org/hyperledger/fabric/samples/privatedata/TransferAgreement.java @@ -0,0 +1,51 @@ +package org.hyperledger.fabric.samples.privatedata; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.json.JSONObject; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@DataType() +public final class TransferAgreement { + + @Property() + private final String assetID; + + + @Property() + private String buyerID; + + public String getAssetID() { + return assetID; + } + + public String getBuyerID() { + return buyerID; + } + + public TransferAgreement(final String assetID, + final String buyer) { + this.assetID = assetID; + this.buyerID = buyer; + } + + public byte[] serialize() { + String jsonStr = new JSONObject(this).toString(); + return jsonStr.getBytes(UTF_8); + } + + public static TransferAgreement deserialize(final byte[] assetJSON) { + try { + JSONObject json = new JSONObject(new String(assetJSON, UTF_8)); + final String id = json.getString("assetID"); + final String buyerID = json.getString("buyerID"); + return new TransferAgreement(id, buyerID); + } catch (Exception e) { + throw new ChaincodeException("Deserialize error: " + e.getMessage(), "DATA_ERROR"); + } + } + + +} diff --git a/asset-transfer-private-data/chaincode-java/src/test/java/org/hyperledger/fabric/samples/privatedata/AssetTransferTest.java b/asset-transfer-private-data/chaincode-java/src/test/java/org/hyperledger/fabric/samples/privatedata/AssetTransferTest.java new file mode 100644 index 0000000..a11a5f0 --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/src/test/java/org/hyperledger/fabric/samples/privatedata/AssetTransferTest.java @@ -0,0 +1,170 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.privatedata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hyperledger.fabric.samples.privatedata.AssetTransfer.AGREEMENT_KEYPREFIX; +import static org.hyperledger.fabric.samples.privatedata.AssetTransfer.ASSET_COLLECTION_NAME; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.security.cert.CertificateException; +import java.util.HashMap; +import java.util.Map; +import org.hyperledger.fabric.contract.ClientIdentity; +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.CompositeKey; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public final class AssetTransferTest { + + @Nested + class InvokeWriteTransaction { + + @Test + public void createAssetWhenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + Map m = new HashMap(); + m.put("asset_properties", dataAsset1Bytes); + when(ctx.getStub().getTransient()).thenReturn(m); + when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID)) + .thenReturn(dataAsset1Bytes); + + Throwable thrown = catchThrowable(() -> { + contract.CreateAsset(ctx); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 already exists"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_ALREADY_EXISTS".getBytes()); + } + + @Test + public void createAssetWhenNewAssetIsCreated() throws CertificateException, IOException { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getMspId()).thenReturn(testOrgOneMSP); + ClientIdentity ci = mock(ClientIdentity.class); + when(ci.getId()).thenReturn(testOrg1Client); + when(ci.getMSPID()).thenReturn(testOrgOneMSP); + when(ctx.getClientIdentity()).thenReturn(ci); + + Map m = new HashMap(); + m.put("asset_properties", dataAsset1Bytes); + when(ctx.getStub().getTransient()).thenReturn(m); + + when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID)) + .thenReturn(new byte[0]); + + Asset created = contract.CreateAsset(ctx); + assertThat(created).isEqualTo(testAsset1); + + verify(stub).putPrivateData(ASSET_COLLECTION_NAME, testAsset1ID, created.serialize()); + } + + @Test + public void transferAssetWhenExistingAssetIsTransferred() throws CertificateException, IOException { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getMspId()).thenReturn(testOrgOneMSP); + ClientIdentity ci = mock(ClientIdentity.class); + when(ci.getId()).thenReturn(testOrg1Client); + when(ctx.getClientIdentity()).thenReturn(ci); + when(ci.getMSPID()).thenReturn(testOrgOneMSP); + final String recipientOrgMsp = "TestOrg2"; + final String buyerIdentity = "TestOrg2User"; + Map m = new HashMap(); + m.put("asset_owner", ("{ \"buyerMSP\": \"" + recipientOrgMsp + "\", \"assetID\": \"" + testAsset1ID + "\" }").getBytes()); + when(ctx.getStub().getTransient()).thenReturn(m); + + when(stub.getPrivateDataHash(anyString(), anyString())).thenReturn("TestHashValue".getBytes()); + when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID)) + .thenReturn(dataAsset1Bytes); + CompositeKey ck = mock(CompositeKey.class); + when(ck.toString()).thenReturn(AGREEMENT_KEYPREFIX + testAsset1ID); + when(stub.createCompositeKey(AGREEMENT_KEYPREFIX, testAsset1ID)).thenReturn(ck); + when(stub.getPrivateData(ASSET_COLLECTION_NAME, AGREEMENT_KEYPREFIX + testAsset1ID)).thenReturn(buyerIdentity.getBytes(UTF_8)); + contract.TransferAsset(ctx); + + Asset exptectedAfterTransfer = Asset.deserialize("{ \"objectType\": \"testasset\", \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"" + buyerIdentity + "\", \"appraisedValue\": 300 }"); + + verify(stub).putPrivateData(ASSET_COLLECTION_NAME, testAsset1ID, exptectedAfterTransfer.serialize()); + String collectionOwner = testOrgOneMSP + "PrivateCollection"; + verify(stub).delPrivateData(collectionOwner, testAsset1ID); + verify(stub).delPrivateData(ASSET_COLLECTION_NAME, AGREEMENT_KEYPREFIX + testAsset1ID); + } + } + + @Nested + class QueryReadAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getPrivateData(ASSET_COLLECTION_NAME, testAsset1ID)) + .thenReturn(dataAsset1Bytes); + + Asset asset = contract.ReadAsset(ctx, testAsset1ID); + + assertThat(asset).isEqualTo(testAsset1); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState(testAsset1ID)).thenReturn(null); + + Asset asset = contract.ReadAsset(ctx, testAsset1ID); + assertThat(asset).isNull(); + } + + @Test + public void invokeUnknownTransaction() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + + Throwable thrown = catchThrowable(() -> { + contract.unknownTransaction(ctx); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Undefined contract method called"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo(null); + + verifyZeroInteractions(ctx); + } + + } + + private static String testOrgOneMSP = "TestOrg1"; + private static String testOrg1Client = "testOrg1User"; + + private static String testAsset1ID = "asset1"; + private static Asset testAsset1 = new Asset("testasset", "asset1", "blue", 5, testOrg1Client); + private static byte[] dataAsset1Bytes = "{ \"objectType\": \"testasset\", \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"testOrg1User\", \"appraisedValue\": 300 }".getBytes(); + +} diff --git a/asset-transfer-private-data/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/asset-transfer-private-data/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..1f0955d --- /dev/null +++ b/asset-transfer-private-data/chaincode-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/asset-transfer-sbe/README.md b/asset-transfer-sbe/README.md new file mode 100644 index 0000000..00549bb --- /dev/null +++ b/asset-transfer-sbe/README.md @@ -0,0 +1,169 @@ +# State-based endorsement asset transfer sample + +Transactions that are submitted to Hyperledger Fabric networks need to be endorsed +by peers that are joined to a channel before the transaction can be added to the +ledger. Fabric peers endorse transactions by executing a smart contract using the +inputs of the transaction proposal. The peers then sign the input and output +generated by the smart contract execution. The endorsement policy specifies the +set of organizations whose peers need to endorse a transaction before it can be +added to the ledger. + +Each chaincode that is deployed to a channel has an endorsement policy that governs +the assets managed by the chaincode smart contracts. However, you can override the +chaincode level endorsement policy to create an endorsement policy for a specific key, +either on the public channel ledger or in a private collection. +State-based endorsement policies, also known as key-level endorsement policies, allow +channel members to use different endorsement policies for assets that are managed by +the same smart contract. For more information about endorsement policies and +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 setStateBasedEndorsementNOutOf(), which can be used as an alternative for setStateBasedEndorsement() inside asset-transfer-sbe smart contracts. + +## About the Sample + +The state-based endorsement (SBE) asset transfer sample demonstrates how to use +key-level endorsement policies to ensure that an asset only is endorsed by an +asset owner. In the course of the tutorial, you will use the smart contract +to complete the following transfer scenario: + +- Deploy the SBE smart contract to a channel that was created using the Fabric +test network. The channel will have two members, Org1 and Org2, that will +participate in the asset transfer. Each organization operates one peer that +is joined to the channel. +- Create an asset using the chaincode endorsement policy. The chaincode level +endorsement policy requires that a majority of organizations on the channel +endorse a transaction. This means that a transaction that creates an asset +needs to be endorsed by peers that belong to Org1 and Org2. When the asset is +created, the smart contract creates a state-based endorsement policy that +specifies that only the organization that owns that asset may endorse update +transactions. Because the asset is owned by Org1, any future updates to the asset +need to be endorsed by the Org1 peer. +- Update the asset by only endorsing with Org1, this will use the state-based +endorsement policy applied to the asset when it was created by the chaincode. +- Transfer the asset to Org2. During the execution of the transfer transaction, +the chaincode will create a new state-based endorsement policy that reflects +the new asset owner for the asset. +- Update the asset once more, this time with Org2 as the owner. Because the +state-based endorsement policy has been updated, this transaction only needs +to be endorsed by Org2. + +## Deploy the smart contract + +We are going to run the SBE smart contract 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 and create a channel named `mychannel`: + +``` +./network.sh up createChannel +``` + +You can use the test network script to deploy the smart contract to the channel that was just created. The script uses the Fabric chaincode lifecycle to deploy the smart contract to the channel. We will use the default chaincode level endorsement policy used by the Fabric chaincode lifecycle, which requires an endorsement from a majority of channel members. In our case, this will require that both Org1 and Org2 endorse a transaction (2 of 2). Deploy the smart contract to `mychannel` using the following command: +``` +./network.sh deployCC -ccn sbe -ccp ../asset-transfer-sbe/chaincode-typescript/ -ccl typescript +``` + +Set the following environment variables to interact with the network as a user from Org1: + +``` +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/User1@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 transfer scenario + +We can now invoke the SBE smart contract to create a new asset: +``` +peer chaincode invoke -o localhost:7050 --waitForEvent --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 sbe --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 -c '{"function":"CreateAsset","Args":["asset1","100","Org1User1"]}' +``` +The create transaction needs to target both peers from Org1 and Org2 to meet the chaincode endorsement policy. The chaincode will read the MSP ID of the client user submitting the transaction and assign that organization as the asset owner. As a result, the asset will initially be owned by Org1. + +You can query the asset using with the following command: +``` +peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}' +``` +The result is a new asset owned by Org1, identified using the Org1 MSP ID `Org1MSP`: +`{"ID":"asset1","Value":100,"Owner":"Org1User1","OwnerOrg":"Org1MSP"}` + +In addition to creating the asset, the `CreateAsset` function also sets a state-based endorsement policy for the asset. Only a peer of the asset owner, can successfully endorse an asset update. To demonstrate the key-level endorsement policy, lets try to update the asset while targeting the Org2 peer: +``` +peer chaincode invoke -o localhost:7050 --waitForEvent --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 sbe --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","200"]}' +``` +The result is an endorsement policy failure: +``` +Error: transaction invalidated with status (ENDORSEMENT_POLICY_FAILURE) - proposal response: +``` + +If we attempt to update the asset with an endorsement from the Org1 peer, the update succeeds: +``` +peer chaincode invoke -o localhost:7050 --waitForEvent --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 sbe --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","200"]}' +``` +You can query the asset one more time to verify that the update was successful: +``` +peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}' +``` + +The asset value is now 200: +``` +{"ID":"asset1","Value":200,"Owner":"Org1User1","OwnerOrg":"Org1MSP"} +``` + +Now that we have tested the asset key-level endorsement policy, we can transfer the asset to Org2. Run the following command to transfer the asset from Org1 to Org2. This time the Org2 MSP ID is provided as a transaction input. The `TransferAsset` function will update the endorsement policy to specify that only a peer of the new owner can update the asset. Note that this command targets the Org1 peer. + +``` +peer chaincode invoke -o localhost:7050 --waitForEvent --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 sbe --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -c '{"function":"TransferAsset","Args":["asset1","Org2User1","Org2MSP"]}' +``` + +We can query the asset to see that the owner has been updated from Org1 to Org2: +``` +peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}' +``` + +The owning organization is now Org2: +``` +{"ID":"asset1","Value":200,"Owner":"Org2User1","OwnerOrg":"Org2MSP"} +``` + +Org2 now needs to endorse any asset updates. Run the following command to try to update the asset with an endorsement from the Org1 peer: +``` +peer chaincode invoke -o localhost:7050 --waitForEvent --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 sbe --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","300"]}' +``` + +The response will be an endorsement policy failure: +``` +Error: transaction invalidated with status (ENDORSEMENT_POLICY_FAILURE) - proposal response: +``` + +Now try to update the asset with an endorsement from the Org2 peer: +``` +peer chaincode invoke -o localhost:7050 --waitForEvent --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 sbe --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","300"]}' +``` + +You can query the asset again to verify that the transaction update succeeded: +``` +peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}' +``` + +The asset value is now 300: +``` +{"ID":"asset1","Value":300,"Owner":"Org2User1","OwnerOrg":"Org2MSP"} +``` + +Note that the transaction to update the asset was submitted by a user from Org1, even though the asset was owned by Org2. The transfer enabled by the SBE smart contract is a simple scenario meant only to demonstrate the use of state-based endorsement policies. The smart contract can use access control to specify that an asset can only be updated by its owner. Private data collections can also be used to ensure that transfers need to be endorsed by the owner and recipient of the transfer, instead of just the asset owner. For a more realistic example of an asset transfer scenario, see the [Secured asset transfer in Fabric](https://hyperledger-fabric.readthedocs.io/en/master/secured_asset_transfer/secured_private_asset_transfer_tutorial.html) tutorial. + +## 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 +``` diff --git a/asset-transfer-sbe/application-javascript/.eslintignore b/asset-transfer-sbe/application-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/asset-transfer-sbe/application-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-sbe/application-javascript/.eslintrc.js b/asset-transfer-sbe/application-javascript/.eslintrc.js new file mode 100644 index 0000000..072edaf --- /dev/null +++ b/asset-transfer-sbe/application-javascript/.eslintrc.js @@ -0,0 +1,37 @@ +/* + * 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', + 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'] + } +}; diff --git a/asset-transfer-sbe/application-javascript/.gitignore b/asset-transfer-sbe/application-javascript/.gitignore new file mode 100644 index 0000000..21b287f --- /dev/null +++ b/asset-transfer-sbe/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-sbe/application-javascript/app.js b/asset-transfer-sbe/application-javascript/app.js new file mode 100644 index 0000000..47adcca --- /dev/null +++ b/asset-transfer-sbe/application-javascript/app.js @@ -0,0 +1,349 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +/** + * A test application to show state based endorsements operations with a running + * asset-transfer-sbe chaincode with discovery. + * -- How to submit a transaction + * -- How to query + * -- How to limit the organizations involved in a transaction + * + * To see the SDK workings, try setting the logging to show on the console before running + * export HFC_LOGGING='{"debug":"console"}' + */ + +// 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-sbe chaincodes deployed on the channel "mychannel" +// with the chaincode name of "sbe". 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 sbe -ccp ../asset-transfer-sbe/chaincode-typescript/ -ccl typescript +// - Be sure that node.js is installed +// ===> from directory /fabric-samples/asset-transfer-sbe/application-javascript +// node -v +// - npm installed code dependencies +// ===> from directory /fabric-samples/asset-transfer-sbe/application-javascript +// npm install +// - to run this test application +// ===> from directory /fabric-samples/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. +// + +const { Gateway, Wallets } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const channelName = 'mychannel'; +const chaincodeName = 'sbe'; + +const org1 = 'Org1MSP'; +const org2 = 'Org2MSP'; +const Org1UserId = 'appUser1'; +const Org2UserId = 'appUser2'; + +async function initGatewayForOrg1() { + 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) + 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(); + //connect using Discovery enabled + await gatewayOrg1.connect(ccpOrg1, + { wallet: walletOrg1, identity: Org1UserId, discovery: { enabled: true, asLocalhost: true } }); + + return gatewayOrg1; + } catch (error) { + console.error(`Error in connecting to gateway for Org1: ${error}`); + process.exit(1); + } +} + +async function initGatewayForOrg2() { + console.log('\n--> Fabric client user & Gateway init: Using Org2 identity to Org2 Peer'); + 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, org2); + await registerAndEnrollUser(caOrg2Client, walletOrg2, org2, Org2UserId, 'org2.department1'); + + try { + // Create a new gateway for connecting to Org's peer node. + const gatewayOrg2 = new Gateway(); + await gatewayOrg2.connect(ccpOrg2, + { wallet: walletOrg2, identity: Org2UserId, discovery: { enabled: true, asLocalhost: true } }); + + return gatewayOrg2; + } catch (error) { + console.error(`Error in connecting to gateway for Org2: ${error}`); + process.exit(1); + } +} + +function checkAsset(org, assetKey, resultBuffer, value, ownerOrg) { + let asset; + if (resultBuffer) { + asset = JSON.parse(resultBuffer.toString('utf8')); + } + + if (asset && value) { + if (asset.Value === value && asset.OwnerOrg === ownerOrg) { + console.log(`*** Result from ${org} - asset ${asset.ID} has value of ${asset.Value} and owned by ${asset.OwnerOrg}`); + } else { + console.log(`*** Failed from ${org} - asset ${asset.ID} has value of ${asset.Value} and owned by ${asset.OwnerOrg}`); + } + } else if (!asset && value === 0 ) { + console.log(`*** Success from ${org} - asset ${assetKey} does not exist`); + } else { + console.log('*** Failed - asset read failed'); + } +} + +async function readAssetByBothOrgs(assetKey, value, ownerOrg, contractOrg1, contractOrg2) { + if (value) { + console.log(`\n--> Evaluate Transaction: ReadAsset, - ${assetKey} should have a value of ${value} and owned by ${ownerOrg}`); + } else { + console.log(`\n--> Evaluate Transaction: ReadAsset, - ${assetKey} should not exist`); + } + let resultBuffer; + resultBuffer = await contractOrg1.evaluateTransaction('ReadAsset', assetKey); + checkAsset('Org1', assetKey, resultBuffer, value, ownerOrg); + resultBuffer = await contractOrg2.evaluateTransaction('ReadAsset', assetKey); + checkAsset('Org2', assetKey, resultBuffer, value, ownerOrg); +} + +// This application uses fabric-samples/test-network based setup and the companion chaincode +// For this illustration, both Org1 & Org2 client identities will be used, however +// notice they are used by two different "gateway"s to simulate two different running +// applications from two different organizations. +async function main() { + try { + // use a random key so that we can run multiple times + const assetKey = `asset-${Math.floor(Math.random() * 100) + 1}`; + + /** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */ + const gatewayOrg1 = await initGatewayForOrg1(); + const networkOrg1 = await gatewayOrg1.getNetwork(channelName); + const contractOrg1 = networkOrg1.getContract(chaincodeName); + + /** ******* Fabric client init: Using Org2 identity to Org2 Peer ******* */ + const gatewayOrg2 = await initGatewayForOrg2(); + const networkOrg2 = await gatewayOrg2.getNetwork(channelName); + const contractOrg2 = networkOrg2.getContract(chaincodeName); + + try { + let transaction; + + try { + // Create an asset by organization Org1, this will require that both organization endorse. + // The endorsement will be handled by Discovery, since the gateway was connected with discovery enabled. + console.log(`\n--> Submit Transaction: CreateAsset, ${assetKey} as Org1 - endorsed by Org1 and Org2`); + await contractOrg1.submitTransaction('CreateAsset', assetKey, '100', 'Tom'); + console.log('*** Result: committed, now asset will only require Org1 to endorse'); + } catch (createError) { + console.log(`*** Failed: create - ${createError}`); + } + + await readAssetByBothOrgs(assetKey, 100, org1, contractOrg1, contractOrg2); + + try { + // Since the gateway is using discovery we should limit the organizations used by + // discovery to endorse. This way we only have to know the organization and not + // the actual peers that may be active at any given time. + console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org1 - endorse by Org1`); + transaction = contractOrg1.createTransaction('UpdateAsset'); + transaction.setEndorsingOrganizations(org1); + await transaction.submit(assetKey, '200'); + console.log('*** Result: committed'); + } catch (updateError) { + console.log(`*** Failed: update - ${updateError}`); + } + + await readAssetByBothOrgs(assetKey, 200, org1, contractOrg1, contractOrg2); + + try { + // Submit a transaction to make an update to the asset that has a key-level endorsement policy + // set to only allow Org1 to make updates. The following example will not use the "setEndorsingOrganizations" + // to limit the organizations that will do the endorsement, this means that it will be sent to all + // organizations in the chaincode endorsement policy. When Org1 endorses, the transaction will be committed + // if Org2 endorses or not. + console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org1 - endorse by Org1 and Org2`); + transaction = contractOrg1.createTransaction('UpdateAsset'); + await transaction.submit(assetKey, '300'); + console.log('*** Result: committed - because Org1 and Org2 both endorsed, while only the Org1 endorsement was required and checked'); + } catch (updateError) { + console.log(`*** Failed: update - ${updateError}`); + } + + await readAssetByBothOrgs(assetKey, 300, org1, contractOrg1, contractOrg2); + + try { + // Again submit the change to both Organizations by not using "setEndorsingOrganizations". Since only + // Org1 is required to approve, the transaction will be committed. + console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org2 - endorse by Org1 and Org2`); + transaction = contractOrg2.createTransaction('UpdateAsset'); + await transaction.submit(assetKey, '400'); + console.log('*** Result: committed - because Org1 was on the discovery list, Org2 did not endorse'); + } catch (updateError) { + console.log(`*** Failed: update - ${updateError}`); + } + + await readAssetByBothOrgs(assetKey, 400, org1, contractOrg1, contractOrg2); + + try { + // Try to update by sending only to Org2, since the state-based-endorsement says that + // Org1 is the only organization allowed to update, the transaction will fail. + console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org2 - endorse by Org2`); + transaction = contractOrg2.createTransaction('UpdateAsset'); + transaction.setEndorsingOrganizations(org2); + await transaction.submit(assetKey, '500'); + console.log('*** Failed: committed - this should have failed to endorse and commit'); + } catch (updateError) { + console.log(`*** Successfully caught the error: \n ${updateError}`); + } + + await readAssetByBothOrgs(assetKey, 400, org1, contractOrg1, contractOrg2); + + try { + // Make a change to the state-based-endorsement policy making Org2 the owner. + console.log(`\n--> Submit Transaction: TransferAsset ${assetKey}, as Org1 - endorse by Org1`); + transaction = contractOrg1.createTransaction('TransferAsset'); + transaction.setEndorsingOrganizations(org1); + await transaction.submit(assetKey, 'Henry', org2); + console.log('*** Result: committed'); + } catch (transferError) { + console.log(`*** Failed: transfer - ${transferError}`); + } + + await readAssetByBothOrgs(assetKey, 400, org2, contractOrg1, contractOrg2); + + try { + // Make sure that Org2 can now make updates, notice how the transaction has limited the + // endorsement to only Org2. + console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org2 - endorse by Org2`); + transaction = contractOrg2.createTransaction('UpdateAsset'); + transaction.setEndorsingOrganizations(org2); + await transaction.submit(assetKey, '600'); + console.log('*** Result: committed'); + } catch (updateError) { + console.log(`*** Failed: update - ${updateError}`); + } + + await readAssetByBothOrgs(assetKey, 600, org2, contractOrg1, contractOrg2); + + try { + // With Org2 now the owner and the state-based-endorsement policy only allowing organization Org2 + // to make updates, a transaction only to Org1 will fail. + console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org1 - endorse by Org1`); + transaction = contractOrg1.createTransaction('UpdateAsset'); + transaction.setEndorsingOrganizations(org1); + await transaction.submit(assetKey, '700'); + console.log('*** Failed: committed - this should have failed to endorse and commit'); + } catch (updateError) { + console.log(`*** Successfully caught the error: \n ${updateError}`); + } + + await readAssetByBothOrgs(assetKey, 600, org2, contractOrg1, contractOrg2); + + try { + // With Org2 the owner and the state-based-endorsement policy only allowing organization Org2 + // to make updates, a transaction to delete by Org1 will fail. + console.log(`\n--> Submit Transaction: DeleteAsset ${assetKey}, as Org1 - endorse by Org1`); + transaction = contractOrg1.createTransaction('DeleteAsset'); + transaction.setEndorsingOrganizations(org1); + await transaction.submit(assetKey); + console.log('*** Failed: committed - this should have failed to endorse and commit'); + } catch (updateError) { + console.log(`*** Successfully caught the error: \n ${updateError}`); + } + + try { + // With Org2 the owner and the state-based-endorsement policy only allowing organization Org2 + // to make updates, a transaction to delete by Org2 will succeed. + console.log(`\n--> Submit Transaction: DeleteAsset ${assetKey}, as Org2 - endorse by Org2`); + transaction = contractOrg2.createTransaction('DeleteAsset'); + transaction.setEndorsingOrganizations(org2); + await transaction.submit(assetKey); + console.log('*** Result: committed'); + } catch (deleteError) { + console.log(`*** Failed: delete - ${deleteError}`); + } + + // The asset should now be deleted, both orgs should not be able to read it + try { + await readAssetByBothOrgs(assetKey, 0, org2, contractOrg1, contractOrg2); + } catch (readDeleteError) { + console.log(`*** Successfully caught the error: ${readDeleteError}`); + } + + } catch (runError) { + console.error(`Error in transaction: ${runError}`); + if (runError.stack) { + console.error(runError.stack); + } + process.exit(1); + } finally { + // Disconnect from the gateway peer when all work for this client identity is complete + gatewayOrg1.disconnect(); + gatewayOrg2.disconnect(); + } + } catch (error) { + console.error(`Error in setup: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +} + +main(); diff --git a/asset-transfer-sbe/application-javascript/package.json b/asset-transfer-sbe/application-javascript/package.json new file mode 100644 index 0000000..aa8cefb --- /dev/null +++ b/asset-transfer-sbe/application-javascript/package.json @@ -0,0 +1,16 @@ +{ + "name": "asset-transfer-sbe", + "version": "1.0.0", + "description": "Asset transfer state based endorsement application implemented in JavaScript", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4" + } +} diff --git a/asset-transfer-sbe/chaincode-java/.gitattributes b/asset-transfer-sbe/chaincode-java/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/asset-transfer-sbe/chaincode-java/.gitignore b/asset-transfer-sbe/chaincode-java/.gitignore new file mode 100644 index 0000000..ae1478c --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/.gitignore @@ -0,0 +1,10 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +/.classpath +/.gradle/ +/.project +/.settings/ +/bin/ +/build/ diff --git a/asset-transfer-sbe/chaincode-java/build.gradle b/asset-transfer-sbe/chaincode-java/build.gradle new file mode 100644 index 0000000..5f90c5a --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/build.gradle @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'application' + id 'checkstyle' + id 'jacoco' +} + +group 'org.hyperledger.fabric.samples' +version '1.0-SNAPSHOT' + +dependencies { + + compileOnly 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + implementation 'com.owlike:genson:1.5' + testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +repositories { + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + jcenter() + maven { + url 'https://jitpack.io' + } +} + +application { + mainClass = 'org.hyperledger.fabric.contract.ContractRouter' +} + +checkstyle { + toolVersion '8.21' + configFile file("config/checkstyle/checkstyle.xml") +} + +checkstyleMain { + source ='src/main/java' +} + +checkstyleTest { + source ='src/test/java' +} + +jacocoTestReport { + dependsOn test +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.9 + } + } + } + + finalizedBy jacocoTestReport +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +check.dependsOn jacocoTestCoverageVerification +installDist.dependsOn check diff --git a/asset-transfer-sbe/chaincode-java/config/checkstyle/checkstyle.xml b/asset-transfer-sbe/chaincode-java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..797da97 --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/config/checkstyle/checkstyle.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/asset-transfer-sbe/chaincode-java/config/checkstyle/suppressions.xml b/asset-transfer-sbe/chaincode-java/config/checkstyle/suppressions.xml new file mode 100644 index 0000000..8c44b0a --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/config/checkstyle/suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/asset-transfer-sbe/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-sbe/chaincode-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/asset-transfer-sbe/chaincode-java/gradlew.bat b/asset-transfer-sbe/chaincode-java/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/asset-transfer-sbe/chaincode-java/settings.gradle b/asset-transfer-sbe/chaincode-java/settings.gradle new file mode 100644 index 0000000..e435c09 --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/settings.gradle @@ -0,0 +1,5 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'sbe' diff --git a/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/Asset.java b/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/Asset.java new file mode 100644 index 0000000..b49d692 --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/Asset.java @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.sbe; + +import java.util.Objects; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +import com.owlike.genson.annotation.JsonProperty; + +@DataType() +public final class Asset { + + @Property() + private final String ID; + + @Property() + private int Value; + + @Property() + private String Owner; + + @Property() + private String OwnerOrg; + + @JsonProperty("ID") + public String getID() { + return ID; + } + + @JsonProperty("Value") + public int getValue() { + return Value; + } + + public void setValue(final int Value) { + this.Value = Value; + } + + @JsonProperty("Owner") + public String getOwner() { + return Owner; + } + + public void setOwner(final String Owner) { + this.Owner = Owner; + } + + @JsonProperty("OwnerOrg") + public String getOwnerOrg() { + return OwnerOrg; + } + + public void setOwnerOrg(final String OwnerOrg) { + this.OwnerOrg = OwnerOrg; + } + + public Asset(@JsonProperty("ID") final String ID, @JsonProperty("Value") final int Value, + @JsonProperty("Owner") final String Owner, @JsonProperty("OwnerOrg") final String OwnerOrg) { + this.ID = ID; + this.Value = Value; + this.Owner = Owner; + this.OwnerOrg = OwnerOrg; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Asset asset = (Asset) o; + return getValue() == asset.getValue() + && + getID().equals(asset.getID()) + && + getOwner().equals(asset.getOwner()) + && + getOwnerOrg().equals(asset.getOwnerOrg()); + } + + @Override + public int hashCode() { + return Objects.hash(getID(), getValue(), getOwner(), getOwnerOrg()); + } + + @Override + public String toString() { + return "Asset{" + "ID='" + ID + '\'' + ", Value=" + Value + ", Owner='" + + Owner + '\'' + ", OwnerOrg='" + OwnerOrg + '\'' + '}'; + } +} 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 new file mode 100644 index 0000000..9843ee3 --- /dev/null +++ b/asset-transfer-sbe/chaincode-java/src/main/java/org/hyperledger/fabric/samples/sbe/AssetContract.java @@ -0,0 +1,255 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.sbe; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.protos.common.MspPrincipal; +import org.hyperledger.fabric.protos.common.Policies; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ext.sbe.StateBasedEndorsement; +import org.hyperledger.fabric.shim.ext.sbe.impl.StateBasedEndorsementFactory; + +import com.owlike.genson.Genson; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +@Contract( + name = "sbe", + info = @Info( + title = "Asset Contract", + description = "Asset Transfer Smart Contract, using State Based Endorsement(SBE), implemented in Java", + version = "0.0.1-SNAPSHOT", + license = @License( + name = "Apache 2.0 License", + url = "http://www.apache.org/licenses/LICENSE-2.0.html"))) +@Default +public final class AssetContract implements ContractInterface { + + private final Genson genson = new Genson(); + + private enum AssetTransferErrors { + ASSET_NOT_FOUND, + ASSET_ALREADY_EXISTS + } + + /** + * Creates a new asset. + * Sets the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates. + * Optionally, set the endorsement policy of the assetId Key, such that any 1(N) out of the Org's specified can endorse future updates. + * + * @param ctx the transaction context + * @param assetId the id of the new asset + * @param value the value of the new asset + * @param owner the owner of the new asset + * @return the created asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset CreateAsset(final Context ctx, final String assetId, final int value, final String owner) { + ChaincodeStub stub = ctx.getStub(); + + if (AssetExists(ctx, assetId)) { + String errorMessage = String.format("Asset %s already exists", assetId); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString()); + } + + final String ownerOrg = getClientOrgId(ctx); + Asset asset = new Asset(assetId, value, owner, ownerOrg); + String assetJSON = genson.serialize(asset); + stub.putStringState(assetId, assetJSON); + + // 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 Org (N) out of the specified Orgs can endorse future updates + // setStateBasedEndorsementNOutOf(ctx, assetId, 1, new String[]{"Org1MSP", "Org2MSP"}); + + return asset; + } + + /** + * Retrieves an asset with the given assetId. + * + * @param ctx the transaction context + * @param assetId the id of the asset + * @return the asset found on the ledger if there was one + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String ReadAsset(final Context ctx, final String assetId) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetId); + + if (assetJSON == null || assetJSON.isEmpty()) { + String errorMessage = String.format("Asset %s does not exist", assetId); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + return assetJSON; + } + + /** + * Updates the properties of an existing asset. + * Needs an endorsement of current owner Org Peer. + * + * @param ctx the transaction context + * @param assetId the id of the asset being updated + * @param newValue the value of the asset being updated + * @return the updated asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset UpdateAsset(final Context ctx, final String assetId, final int newValue) { + ChaincodeStub stub = ctx.getStub(); + + String assetString = ReadAsset(ctx, assetId); + Asset asset = genson.deserialize(assetString, Asset.class); + asset.setValue(newValue); + String updatedAssetJSON = genson.serialize(asset); + stub.putStringState(assetId, updatedAssetJSON); + + return asset; + } + + /** + * Deletes the given asset. + * Needs an endorsement of current owner Org Peer. + * + * @param ctx the transaction context + * @param assetId the id of the asset being deleted + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void DeleteAsset(final Context ctx, final String assetId) { + ChaincodeStub stub = ctx.getStub(); + + if (!AssetExists(ctx, assetId)) { + String errorMessage = String.format("Asset %s does not exist", assetId); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + stub.delState(assetId); + } + + /** + * Updates the owner & ownerOrg field of asset with given assetId, ownerOrg must be a valid Org MSP Id. + * Needs an endorsement of current owner Org Peer. + * Re-sets the endorsement policy of the assetId Key, such that new owner Org Peer is required to endorse future updates. + * + * @param ctx the transaction context + * @param assetId the id of the asset being transferred + * @param newOwner the new owner + * @param newOwnerOrg the new owner Org MSPID + * @return the updated asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset TransferAsset(final Context ctx, final String assetId, final String newOwner, final String newOwnerOrg) { + ChaincodeStub stub = ctx.getStub(); + + String assetString = ReadAsset(ctx, assetId); + Asset asset = genson.deserialize(assetString, Asset.class); + asset.setOwner(newOwner); + asset.setOwnerOrg(newOwnerOrg); + String updatedAssetJSON = genson.serialize(asset); + stub.putStringState(assetId, updatedAssetJSON); + + // 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; + } + + /** + * Checks the existence of the asset. + * + * @param ctx the transaction context + * @param assetId the id of the asset + * @return boolean indicating the existence of the asset + */ + private boolean AssetExists(final Context ctx, final String assetId) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetId); + + return (assetJSON != null && !assetJSON.isEmpty()); + } + + /** + * Retrieves the client's OrgId (MSPID) + * + * @param ctx the transaction context + * @return String value of the Org MSPID + */ + private static String getClientOrgId(final Context ctx) { + return ctx.getClientIdentity().getMSPID(); + } + + /** + * Sets an endorsement policy to the 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 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()); + } + + /** + * Sets an endorsement policy to the 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 setStateBasedEndorsementNOutOf(final Context ctx, final String assetId, final int nOrgs, final String[] ownerOrgs) { + ctx.getStub().setStateValidationParameter(assetId, policy(nOrgs, Arrays.asList(ownerOrgs))); + } + + /** + * 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 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) { + mspids.sort(Comparator.naturalOrder()); + final List principals = new ArrayList<>(); + final List signPolicy = new ArrayList<>(); + for (int i = 0; i < mspids.size(); i++) { + final String mspid = mspids.get(i); + principals.add(MspPrincipal.MSPPrincipal.newBuilder().setPrincipalClassification(MspPrincipal.MSPPrincipal.Classification.ROLE) + .setPrincipal(MspPrincipal.MSPRole.newBuilder().setMspIdentifier(mspid).setRole(MspPrincipal.MSPRole.MSPRoleType.MEMBER).build().toByteString()).build()); + signPolicy.add(signedBy(i)); + } + // Create the policy such that it requires any N signature's from all of the principals provided + return Policies.SignaturePolicyEnvelope.newBuilder().setVersion(0).setRule(nOutOf(nOrgs, signPolicy)) + .addAllIdentities(principals).build().toByteArray(); + } + + private static Policies.SignaturePolicy signedBy(final int index) { + return Policies.SignaturePolicy.newBuilder().setSignedBy(index).build(); + } + + private static Policies.SignaturePolicy nOutOf(final int n, final List policies) { + return Policies.SignaturePolicy.newBuilder().setNOutOf(Policies.SignaturePolicy.NOutOf.newBuilder().setN(n).addAllRules(policies).build()).build(); + } +} diff --git a/asset-transfer-sbe/chaincode-typescript/.gitignore b/asset-transfer-sbe/chaincode-typescript/.gitignore new file mode 100644 index 0000000..2a76b3d --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/.gitignore @@ -0,0 +1,20 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +# Compiled TypeScript files +dist + +# Editor Config +.editorconfig + +# npm ignore +.npmignore diff --git a/asset-transfer-sbe/chaincode-typescript/package.json b/asset-transfer-sbe/chaincode-typescript/package.json new file mode 100644 index 0000000..43be5e7 --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/package.json @@ -0,0 +1,65 @@ +{ + "name": "asset-transfer-sbe", + "version": "0.0.1", + "description": "Asset Transfer contract, using State Based Endorsement(SBE), implemented in TypeScript", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "pretest": "npm run lint", + "test": "nyc mocha -r ts-node/register src/**/*.spec.ts", + "start": "fabric-chaincode-node start", + "build": "tsc", + "build:watch": "tsc -w", + "prepublishOnly": "npm run build" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "@types/chai": "^4.2.11", + "@types/chai-as-promised": "^7.1.2", + "@types/mocha": "^7.0.2", + "@types/node": "^13.9.3", + "@types/sinon": "^7.5.2", + "@types/sinon-chai": "^3.2.3", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "mocha": "^7.1.1", + "nyc": "^15.0.0", + "sinon": "^9.0.1", + "sinon-chai": "^3.5.0", + "ts-node": "^8.8.1", + "tslint": "^6.1.0", + "typescript": "^3.8.3", + "winston": "^3.2.1" + }, + "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-sbe/chaincode-typescript/src/asset.ts b/asset-transfer-sbe/chaincode-typescript/src/asset.ts new file mode 100644 index 0000000..58bd07a --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/src/asset.ts @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Object, Property } from 'fabric-contract-api'; + +@Object() +export class Asset { + @Property() + public ID: string; + + @Property() + public Value: number; + + @Property() + public Owner: string; + + @Property() + public OwnerOrg: string; +} diff --git a/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts b/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts new file mode 100644 index 0000000..493f85c --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +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 { + + // CreateAsset creates a new asset + // CreateAsset sets the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates + @Transaction() + public async CreateAsset(ctx: Context, assetId: string, value: number, owner: string): Promise { + const exists = await this.AssetExists(ctx, assetId); + if (exists) { + throw new Error(`The asset ${assetId} already exists`); + } + const ownerOrg = AssetContract.getClientOrgId(ctx); + const asset = new Asset(); + asset.ID = assetId; + asset.Value = value; + asset.Owner = owner; + asset.OwnerOrg = ownerOrg; + 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 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 + @Transaction(false) + public async ReadAsset(ctx: Context, assetId: string): Promise { + const exists = await this.AssetExists(ctx, assetId); + if (!exists) { + throw new Error(`The asset ${assetId} does not exist`); + } + // Read the asset + const assetJSON = await ctx.stub.getState(assetId); + return assetJSON.toString(); + } + + // UpdateAsset updates an existing asset + // UpdateAsset needs an endorsement of current owner Org Peer + @Transaction() + public async UpdateAsset(ctx: Context, assetId: string, newValue: number): Promise { + const assetString = await this.ReadAsset(ctx, assetId); + const asset = JSON.parse(assetString) as Asset; + asset.Value = newValue; + const buffer = Buffer.from(JSON.stringify(asset)); + // Update the asset + await ctx.stub.putState(assetId, buffer); + } + + // DeleteAsset deletes an given asset + // DeleteAsset needs an endorsement of current owner Org Peer + @Transaction() + public async DeleteAsset(ctx: Context, assetId: string): Promise { + const exists = await this.AssetExists(ctx, assetId); + if (!exists) { + throw new Error(`The asset ${assetId} does not exist`); + } + // Delete the asset + await ctx.stub.deleteState(assetId); + } + + // TransferAsset updates the Owner & OwnerOrg field of asset with given assetId, OwnerOrg must be a valid Org MSP Id + // TransferAsset needs an endorsement of current owner Org Peer + // TransferAsset re-sets the endorsement policy of the assetId Key, such that new owner Org Peer is required to endorse future updates + @Transaction() + public async TransferAsset(ctx: Context, assetId: string, newOwner: string, newOwnerOrg: string): Promise { + const assetString = await this.ReadAsset(ctx, assetId); + const asset = JSON.parse(assetString) as Asset; + asset.Owner = newOwner; + asset.OwnerOrg = newOwnerOrg; + // 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.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 + public async AssetExists(ctx: Context, assetId: string): Promise { + const buffer = await ctx.stub.getState(assetId); + return (!!buffer && buffer.length > 0); + } + + // 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()); + } + + // 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(); + } +} diff --git a/asset-transfer-sbe/chaincode-typescript/src/index.ts b/asset-transfer-sbe/chaincode-typescript/src/index.ts new file mode 100644 index 0000000..41a40ec --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/src/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AssetContract } from './assetContract'; +export { AssetContract } from './assetContract'; + +export const contracts: any[] = [ AssetContract ]; diff --git a/asset-transfer-sbe/chaincode-typescript/tsconfig.json b/asset-transfer-sbe/chaincode-typescript/tsconfig.json new file mode 100644 index 0000000..7201e49 --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "es2017", + "moduleResolution": "node", + "module": "commonjs", + "declaration": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/asset-transfer-sbe/chaincode-typescript/tslint.json b/asset-transfer-sbe/chaincode-typescript/tslint.json new file mode 100644 index 0000000..2044fa6 --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/tslint.json @@ -0,0 +1,21 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "indent": [true, "spaces", 4], + "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"], + "max-line-length": false + }, + "rulesDirectory": [] +} diff --git a/asset-transfer-secured-agreement/application-javascript/.eslintignore b/asset-transfer-secured-agreement/application-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/asset-transfer-secured-agreement/application-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-secured-agreement/application-javascript/.eslintrc.js b/asset-transfer-secured-agreement/application-javascript/.eslintrc.js new file mode 100644 index 0000000..072edaf --- /dev/null +++ b/asset-transfer-secured-agreement/application-javascript/.eslintrc.js @@ -0,0 +1,37 @@ +/* + * 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', + 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'] + } +}; diff --git a/asset-transfer-secured-agreement/application-javascript/.gitignore b/asset-transfer-secured-agreement/application-javascript/.gitignore new file mode 100644 index 0000000..21b287f --- /dev/null +++ b/asset-transfer-secured-agreement/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-secured-agreement/application-javascript/app.js b/asset-transfer-secured-agreement/application-javascript/app.js new file mode 100644 index 0000000..fca8579 --- /dev/null +++ b/asset-transfer-secured-agreement/application-javascript/app.js @@ -0,0 +1,567 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +/** + * Application 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 + * -- How to submit a transaction + * -- How to query + * -- How to limit the organizations involved in a transaction + * + * To see the SDK workings, try setting the logging to show on the console before running + * export HFC_LOGGING='{"debug":"console"}' + */ + +// 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 the asset-transfer-secured-agreement/chaincode-go chaincode deployed on +// the channel "mychannel". The following deploy command will package, install, +// approve, and commit the golang chaincode, all the actions it takes +// to deploy a chaincode to a channel with the endorsement and private collection +// settings. +// ===> from directory /fabric-samples/test-network +// ./network.sh deployCC -ccn secured -ccp ../asset-transfer-secured-agreement/chaincode-go/ -ccl go -ccep "OR('Org1MSP.peer','Org2MSP.peer')" +// +// - Be sure that node.js is installed +// ===> from directory /fabric-samples/asset-transfer-secured-agreement/application-javascript +// node -v +// - npm installed code dependencies +// ===> from directory /fabric-samples/asset-transfer-secured-agreement/application-javascript +// npm install +// - to run this test application +// ===> from directory /fabric-samples/asset-transfer-secured-agreement/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-secured-agreement/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. +// + +const { Gateway, Wallets } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const channelName = 'mychannel'; +const chaincodeName = 'secured'; + +const org1 = 'Org1MSP'; +const org2 = 'Org2MSP'; +const Org1UserId = 'appUser1'; +const Org2UserId = 'appUser2'; + +const RED = '\x1b[31m\n'; +const GREEN = '\x1b[32m\n'; +const RESET = '\x1b[0m'; + +async function initGatewayForOrg1() { + 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(); + //connect using Discovery enabled + await gatewayOrg1.connect(ccpOrg1, + { wallet: walletOrg1, identity: Org1UserId, discovery: { enabled: true, asLocalhost: true } }); + + return gatewayOrg1; + } catch (error) { + console.error(`Error in connecting to gateway for Org1: ${error}`); + process.exit(1); + } +} + +async function initGatewayForOrg2() { + console.log(`${GREEN}--> Fabric client user & Gateway init: Using Org2 identity to Org2 Peer${RESET}`); + 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, org2); + await registerAndEnrollUser(caOrg2Client, walletOrg2, org2, Org2UserId, 'org2.department1'); + + try { + // Create a new gateway for connecting to Org's peer node. + const gatewayOrg2 = new Gateway(); + await gatewayOrg2.connect(ccpOrg2, + { wallet: walletOrg2, identity: Org2UserId, discovery: { enabled: true, asLocalhost: true } }); + + return gatewayOrg2; + } catch (error) { + console.error(`Error in connecting to gateway for Org2: ${error}`); + process.exit(1); + } +} + +async function readPrivateAsset(assetKey, org, contract) { + console.log(`${GREEN}--> Evaluate Transaction: GetAssetPrivateProperties, - ${assetKey} from organization ${org}${RESET}`); + try { + const resultBuffer = await contract.evaluateTransaction('GetAssetPrivateProperties', assetKey); + const asset = JSON.parse(resultBuffer.toString('utf8')); + console.log(`*** Result: GetAssetPrivateProperties, ${JSON.stringify(asset)}`); + + } catch (evalError) { + console.log(`*** Failed evaluateTransaction readPrivateAsset: ${evalError}`); + } +} + +async function readBidPrice(assetKey, org, contract) { + console.log(`${GREEN}--> Evaluate Transaction: GetAssetBidPrice, - ${assetKey} from organization ${org}${RESET}`); + try { + const resultBuffer = await contract.evaluateTransaction('GetAssetBidPrice', assetKey); + const asset = JSON.parse(resultBuffer.toString('utf8')); + console.log(`*** Result: GetAssetBidPrice, ${JSON.stringify(asset)}`); + + } catch (evalError) { + console.log(`*** Failed evaluateTransaction GetAssetBidPrice: ${evalError}`); + } +} + +async function readSalePrice(assetKey, org, contract) { + console.log(`${GREEN}--> Evaluate Transaction: GetAssetSalesPrice, - ${assetKey} from organization ${org}${RESET}`); + try { + const resultBuffer = await contract.evaluateTransaction('GetAssetSalesPrice', assetKey); + const asset = JSON.parse(resultBuffer.toString('utf8')); + console.log(`*** Result: GetAssetSalesPrice, ${JSON.stringify(asset)}`); + + } catch (evalError) { + console.log(`*** Failed evaluateTransaction GetAssetSalesPrice: ${evalError}`); + } +} + +function checkAsset(org, resultBuffer, ownerOrg) { + let asset; + if (resultBuffer) { + asset = JSON.parse(resultBuffer.toString('utf8')); + } + + if (asset) { + if (asset.ownerOrg === ownerOrg) { + console.log(`*** Result from ${org} - asset ${asset.assetID} owned by ${asset.ownerOrg} DESC:${asset.publicDescription}`); + } else { + console.log(`${RED}*** Failed owner check from ${org} - asset ${asset.assetID} owned by ${asset.ownerOrg} DESC:${asset.publicDescription}${RESET}`); + } + } +} + +// This is not a real function for an application, this simulates when two applications are running +// from different organizations and what they would see if they were to both query the asset +async function readAssetByBothOrgs(assetKey, ownerOrg, contractOrg1, contractOrg2) { + console.log(`${GREEN}--> Evaluate Transactions: ReadAsset, - ${assetKey} should be owned by ${ownerOrg}${RESET}`); + let resultBuffer; + resultBuffer = await contractOrg1.evaluateTransaction('ReadAsset', assetKey); + checkAsset('Org1', resultBuffer, ownerOrg); + resultBuffer = await contractOrg2.evaluateTransaction('ReadAsset', assetKey); + checkAsset('Org2', resultBuffer, ownerOrg); +} + +// This application uses fabric-samples/test-network based setup and the companion chaincode +// For this illustration, both Org1 & Org2 client identities will be used, however +// notice they are used by two different "gateway"s to simulate two different running +// applications from two different organizations. +async function main() { + console.log(`${GREEN} **** START ****${RESET}`); + try { + const randomNumber = Math.floor(Math.random() * 100) + 1; + // use a random key so that we can run multiple times + const assetKey = `asset-${randomNumber}`; + + /** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */ + const gatewayOrg1 = await initGatewayForOrg1(); + const networkOrg1 = await gatewayOrg1.getNetwork(channelName); + const contractOrg1 = networkOrg1.getContract(chaincodeName); + + /** ******* Fabric client init: Using Org2 identity to Org2 Peer ******* */ + const gatewayOrg2 = await initGatewayForOrg2(); + const networkOrg2 = await gatewayOrg2.getNetwork(channelName); + const contractOrg2 = networkOrg2.getContract(chaincodeName); + + try { + let transaction; + + try { + // Create an asset by organization Org1, this only requires the owning + // organization to endorse. + // With the gateway using discovery, we should limit the organizations used + // to endorse. This only requires knowledge of the Organizations and not + // the actual peers that may be active at any given time. + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + color: 'blue', + size: 35, + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`); + console.log(`${asset_properties_string}`); + transaction = contractOrg1.createTransaction('CreateAsset'); + transaction.setEndorsingOrganizations(org1); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org1} is not for sale`); + console.log(`*** Result: committed, asset ${assetKey} is owned by Org1`); + } catch (createError) { + console.log(`${RED}*** Failed: CreateAsset - ${createError}${RESET}`); + } + + // read the public details by both orgs + await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2); + // Org1 should be able to read the private data details of this asset + await readPrivateAsset(assetKey, org1, contractOrg1); + // Org2 is not the owner and does not have the private details, this should fail + await readPrivateAsset(assetKey, org2, contractOrg2); + + try { + // This is an update to the public state and requires only the owner to endorse. + console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org1 - endorse by Org1${RESET}`); + transaction = contractOrg1.createTransaction('ChangePublicDescription'); + transaction.setEndorsingOrganizations(org1); + await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org1} is for sale`); + console.log(`*** Result: committed, asset ${assetKey} is now for sale by Org1`); + } catch (updateError) { + console.log(`${RED}*** Failed: ChangePublicDescription - ${updateError}${RESET}`); + } + + // read the public details by both orgs + await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2); + + try { + // This is an update to the public state and requires the owner(Org1) to endorse and + // sent by the owner org client (Org1). + // Since the client is from Org2, which is not the owner, this will fail + console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org2 - endorse by Org2${RESET}`); + transaction = contractOrg2.createTransaction('ChangePublicDescription'); + transaction.setEndorsingOrganizations(org2); + await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org2} is NOT for sale`); + console.log(`${RESET}*** Failed: Org2 is not the owner and this should have failed${RESET}`); + } catch (updateError) { + console.log(`*** Success: ChangePublicDescription has failed endorsememnt by Org2 sent by Org2 - ${updateError}`); + } + + try { + // This is an update to the public state and requires the owner(Org1) to endorse and + // sent by the owner org client (Org1). + // Since this is being sent by Org2, which is not the owner, this will fail + console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org2 - endorse by Org1${RESET}`); + transaction = contractOrg2.createTransaction('ChangePublicDescription'); + transaction.setEndorsingOrganizations(org1); + await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org2} is NOT for sale`); + console.log(`${RESET}*** Failed: Org2 is not the owner and this should have failed${RESET}`); + } catch (updateError) { + console.log(`*** Success: ChangePublicDescription has failed endorsement by Org1 sent by Org2 - ${updateError}`); + } + + // read the public details by both orgs + await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2); + + try { + // Agree to a sell by Org1 + const asset_price = { + asset_id: assetKey, + price: 110, + trade_id: randomNumber.toString() + }; + const asset_price_string = JSON.stringify(asset_price); + console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`); + transaction = contractOrg1.createTransaction('AgreeToSell'); + transaction.setEndorsingOrganizations(org1); + transaction.setTransient({ + asset_price: Buffer.from(asset_price_string) + }); + await transaction.submit(assetKey); + console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 110`); + } catch (sellError) { + console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`); + } + + try { + // check the private information about the asset from Org2 + // Org1 would have to send Org2 these details, so the hash of the + // details may be checked by the chaincode. + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + color: 'blue', + size: 35, + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetKey} as Org2 - endorsed by Org2${RESET}`); + console.log(`${asset_properties_string}`); + transaction = contractOrg2.createTransaction('VerifyAssetProperties'); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + const verifyResultBuffer = await transaction.evaluate(assetKey); + if (verifyResultBuffer) { + const verifyResult = Boolean(verifyResultBuffer.toString()); + if (verifyResult) { + console.log(`*** Successfully VerifyAssetProperties, private information about asset ${assetKey} has been verified by Org2`); + } else { + console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetKey} has not been verified by Org2`); + } + } else { + console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetKey} has not been verified by Org2`); + } + } catch (verifyError) { + console.log(`${RED}*** Failed: VerifyAssetProperties - ${verifyError}${RESET}`); + } + + try { + // Agree to a buy by Org2 + const asset_price = { + asset_id: assetKey, + price: 100, + trade_id: randomNumber.toString() + }; + const asset_price_string = JSON.stringify(asset_price); + console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetKey} as Org2 - endorsed by Org2${RESET}`); + transaction = contractOrg2.createTransaction('AgreeToBuy'); + transaction.setEndorsingOrganizations(org2); + transaction.setTransient({ + asset_price: Buffer.from(asset_price_string) + }); + await transaction.submit(assetKey); + console.log(`*** Result: committed, Org2 has agreed to buy asset ${assetKey} for 100`); + } catch (buyError) { + console.log(`${RED}*** Failed: AgreeToBuy - ${buyError}${RESET}`); + } + + // read the public details by both orgs + await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2); + + // Org1 should be able to read the private data details of this asset + await readPrivateAsset(assetKey, org1, contractOrg1); + // Org2 is not the owner and does not have the private details, this should fail + await readPrivateAsset(assetKey, org2, contractOrg2); + + // Org1 should be able to read the sale price of this asset + await readSalePrice(assetKey, org1, contractOrg1); + // Org2 has not set a sale price and this should fail + await readSalePrice(assetKey, org2, contractOrg2); + + // Org1 has not agreed to buy so this should fail + await readBidPrice(assetKey, org1, contractOrg1); + // Org2 should be able to see the price it has agreed + await readBidPrice(assetKey, org2, contractOrg2); + + try { + // Org1 will try to transfer the asset to Org2 + // This will fail due to the sell price and the bid price + // are not the same + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + color: 'blue', + size: 35, + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + const asset_price = { + asset_id: assetKey, + price: 110, + trade_id: randomNumber.toString() + }; + const asset_price_string = JSON.stringify(asset_price); + + console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`); + console.log(`${asset_properties_string}`); + transaction = contractOrg1.createTransaction('TransferAsset'); + transaction.setEndorsingOrganizations(org1); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string), + asset_price: Buffer.from(asset_price_string) + }); + await transaction.submit(assetKey, org2); + console.log(`${RED}*** Failed: committed, TransferAsset should have failed for asset ${assetKey}${RESET}`); + } catch (transferError) { + console.log(`*** Success: TransferAsset - ${transferError}`); + } + + try { + // Agree to a sell by Org1 + // Org1, the seller will agree to the bid price of Org2 + const asset_price = { + asset_id: assetKey, + price: 100, + trade_id: randomNumber.toString() + }; + const asset_price_string = JSON.stringify(asset_price); + console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`); + transaction = contractOrg1.createTransaction('AgreeToSell'); + transaction.setEndorsingOrganizations(org1); + transaction.setTransient({ + asset_price: Buffer.from(asset_price_string) + }); + await transaction.submit(assetKey); + console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 100`); + } catch (sellError) { + console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`); + } + + // read the public details by both orgs + await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2); + + // Org1 should be able to read the private data details of this asset + await readPrivateAsset(assetKey, org1, contractOrg1); + + // Org1 should be able to read the sale price of this asset + await readSalePrice(assetKey, org1, contractOrg1); + + // Org2 should be able to see the price it has agreed + await readBidPrice(assetKey, org2, contractOrg2); + + try { + // Org2 user will try to transfer the asset to Org2 + // This will fail as the owner is Org1 + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + color: 'blue', + size: 35, + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + const asset_price = { + asset_id: assetKey, + price: 100, + trade_id: randomNumber.toString() + }; + const asset_price_string = JSON.stringify(asset_price); + + console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org2 - endorsed by Org1${RESET}`); + console.log(`${asset_properties_string}`); + transaction = contractOrg2.createTransaction('TransferAsset'); + transaction.setEndorsingOrganizations(org1, org2); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string), + asset_price: Buffer.from(asset_price_string) + }); + await transaction.submit(assetKey, org2); + console.log(`${RED}*** FAILED: committed, TransferAsset - Org2 now owns the asset ${assetKey}${RESET}`); + } catch (transferError) { + console.log(`*** Succeded: TransferAsset - ${transferError}`); + } + + try { + // Org1 will transfer the asset to Org2 + // This will now complete as the sell price and the bid price are the same + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + color: 'blue', + size: 35, + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + const asset_price = { + asset_id: assetKey, + price: 100, + trade_id: randomNumber.toString() + }; + const asset_price_string = JSON.stringify(asset_price); + + console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`); + console.log(`${asset_properties_string}`); + transaction = contractOrg1.createTransaction('TransferAsset'); + transaction.setEndorsingOrganizations(org1, org2); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string), + asset_price: Buffer.from(asset_price_string) + }); + await transaction.submit(assetKey, org2); + console.log(`*** Results: committed, TransferAsset - Org2 now owns the asset ${assetKey}`); + } catch (transferError) { + console.log(`${RED}*** Failed: TransferAsset - ${transferError}${RESET}`); + } + + // read the public details by both orgs + await readAssetByBothOrgs(assetKey, org2, contractOrg1, contractOrg2); + + // Org2 should be able to read the private data details of this asset + await readPrivateAsset(assetKey, org2, contractOrg2); + // Org1 should not be able to read the private data details of this asset + await readPrivateAsset(assetKey, org1, contractOrg1); + + try { + // This is an update to the public state and requires only the owner to endorse. + // Org2 wants to indicate that the items is no longer for sale + console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org2 - endorse by Org2${RESET}`); + transaction = contractOrg2.createTransaction('ChangePublicDescription'); + transaction.setEndorsingOrganizations(org2); + await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org2} is NOT for sale`); + console.log('*** Results: committed - Org2 is now the owner and asset is not for sale'); + } catch (updateError) { + console.log(`${RED}*** Failed: ChangePublicDescription has failed by Org2 - ${updateError}${RESET}`); + } + + // read the public details by both orgs + await readAssetByBothOrgs(assetKey, org2, contractOrg1, contractOrg2); + } catch (runError) { + console.error(`Error in transaction: ${runError}`); + if (runError.stack) { + console.error(runError.stack); + } + process.exit(1); + } finally { + // Disconnect from the gateway peer when all work for this client identity is complete + console.log(`${GREEN}--> Close gateways`); + gatewayOrg1.disconnect(); + gatewayOrg2.disconnect(); + } + } catch (error) { + console.error(`Error in setup: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } + console.log(`${GREEN} **** END ****${RESET}`); +} +main(); diff --git a/asset-transfer-secured-agreement/application-javascript/package.json b/asset-transfer-secured-agreement/application-javascript/package.json new file mode 100644 index 0000000..d342456 --- /dev/null +++ b/asset-transfer-secured-agreement/application-javascript/package.json @@ -0,0 +1,16 @@ +{ + "name": "asset-transfer-secured-agreement", + "version": "1.0.0", + "description": "Javascript application 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", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4" + } +} diff --git a/asset-transfer-secured-agreement/chaincode-go/README.md b/asset-transfer-secured-agreement/chaincode-go/README.md new file mode 100644 index 0000000..f65f09b --- /dev/null +++ b/asset-transfer-secured-agreement/chaincode-go/README.md @@ -0,0 +1 @@ +[Secured asset transfer in Fabric Tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/secured_asset_transfer/secured_private_asset_transfer_tutorial.html) diff --git a/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go new file mode 100644 index 0000000..cce1c2c --- /dev/null +++ b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go @@ -0,0 +1,556 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/hyperledger/fabric-chaincode-go/pkg/statebased" + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +const ( + typeAssetForSale = "S" + typeAssetBid = "B" + typeAssetSaleReceipt = "SR" + typeAssetBuyReceipt = "BR" +) + +type SmartContract struct { + contractapi.Contract +} + +// Asset struct and properties must be exported (start with capitals) to work with contract api metadata +type Asset struct { + ObjectType string `json:"objectType"` // ObjectType is used to distinguish different object types in the same chaincode namespace + ID string `json:"assetID"` + OwnerOrg string `json:"ownerOrg"` + PublicDescription string `json:"publicDescription"` +} + +type receipt struct { + price int + timestamp time.Time +} + +// CreateAsset creates an asset and sets it as owned by the client's org +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, assetID, publicDescription string) error { + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Asset properties must be retrieved from the transient field as they are private + immutablePropertiesJSON, ok := transientMap["asset_properties"] + if !ok { + return fmt.Errorf("asset_properties key not found in the transient map") + } + + // Get client org id and verify it matches peer org id. + // In this scenario, client is only authorized to read/write private data from its own peer. + clientOrgID, err := getClientOrgID(ctx, true) + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + asset := Asset{ + ObjectType: "asset", + ID: assetID, + OwnerOrg: clientOrgID, + PublicDescription: publicDescription, + } + assetBytes, err := json.Marshal(asset) + if err != nil { + return fmt.Errorf("failed to create asset JSON: %v", err) + } + + err = ctx.GetStub().PutState(asset.ID, assetBytes) + if err != nil { + return fmt.Errorf("failed to put asset in public data: %v", err) + } + + // Set the endorsement policy such that an owner org peer is required to endorse future updates + err = setAssetStateBasedEndorsement(ctx, asset.ID, clientOrgID) + if err != nil { + return fmt.Errorf("failed setting state based endorsement for owner: %v", err) + } + + // Persist private immutable asset properties to owner's private data collection + collection := buildCollectionName(clientOrgID) + err = ctx.GetStub().PutPrivateData(collection, asset.ID, immutablePropertiesJSON) + if err != nil { + return fmt.Errorf("failed to put Asset private details: %v", err) + } + + return nil +} + +// ChangePublicDescription updates the assets public description. Only the current owner can update the public description +func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionContextInterface, assetID string, newDescription string) error { + // No need to check client org id matches peer org id, rely on the asset ownership check instead. + clientOrgID, err := getClientOrgID(ctx, false) + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + asset, err := s.ReadAsset(ctx, assetID) + if err != nil { + return fmt.Errorf("failed to get asset: %v", err) + } + + // Auth check to ensure that client's org actually owns the asset + if clientOrgID != asset.OwnerOrg { + return fmt.Errorf("a client from %s cannot update the description of a asset owned by %s", clientOrgID, asset.OwnerOrg) + } + + asset.PublicDescription = newDescription + updatedAssetJSON, err := json.Marshal(asset) + if err != nil { + return fmt.Errorf("failed to marshal asset: %v", err) + } + + return ctx.GetStub().PutState(assetID, updatedAssetJSON) +} + +// AgreeToSell adds seller's asking price to seller's implicit private data collection +func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error { + asset, err := s.ReadAsset(ctx, assetID) + if err != nil { + return err + } + + clientOrgID, err := getClientOrgID(ctx, true) + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + // Verify that this clientOrgId actually owns the asset. + if clientOrgID != asset.OwnerOrg { + return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg) + } + + return agreeToPrice(ctx, assetID, typeAssetForSale) +} + +// AgreeToBuy adds buyer's bid price to buyer's implicit private data collection +func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface, assetID string) error { + return agreeToPrice(ctx, assetID, typeAssetBid) +} + +// agreeToPrice adds a bid or ask price to caller's implicit private data collection +func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error { + // In this scenario, client is only authorized to read/write private data from its own peer. + clientOrgID, err := getClientOrgID(ctx, true) + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + transMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Asset price must be retrieved from the transient field as they are private + price, ok := transMap["asset_price"] + if !ok { + return fmt.Errorf("asset_price key not found in the transient map") + } + + collection := buildCollectionName(clientOrgID) + + // Persist the agreed to price in a collection sub-namespace based on priceType key prefix, + // to avoid collisions between private asset properties, sell price, and buy price + assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + // The Price hash will be verified later, therefore always pass and persist price bytes as is, + // so that there is no risk of nondeterministic marshaling. + err = ctx.GetStub().PutPrivateData(collection, assetPriceKey, price) + if err != nil { + return fmt.Errorf("failed to put asset bid: %v", err) + } + + return nil +} + +// VerifyAssetProperties Allows a buyer to validate the properties of +// an asset against the owner's implicit private data collection +func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) { + transMap, err := ctx.GetStub().GetTransient() + if err != nil { + return false, fmt.Errorf("error getting transient: %v", err) + } + + /// Asset properties must be retrieved from the transient field as they are private + immutablePropertiesJSON, ok := transMap["asset_properties"] + if !ok { + return false, fmt.Errorf("asset_properties key not found in the transient map") + } + + asset, err := s.ReadAsset(ctx, assetID) + if err != nil { + return false, fmt.Errorf("failed to get asset: %v", err) + } + + collectionOwner := buildCollectionName(asset.OwnerOrg) + immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionOwner, assetID) + if err != nil { + return false, fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err) + } + if immutablePropertiesOnChainHash == nil { + return false, fmt.Errorf("asset private properties hash does not exist: %s", assetID) + } + + hash := sha256.New() + hash.Write(immutablePropertiesJSON) + calculatedPropertiesHash := hash.Sum(nil) + + // verify that the hash of the passed immutable properties matches the on-chain hash + if !bytes.Equal(immutablePropertiesOnChainHash, calculatedPropertiesHash) { + return false, fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x", + calculatedPropertiesHash, + immutablePropertiesJSON, + immutablePropertiesOnChainHash, + ) + } + + return true, nil +} + +// TransferAsset checks transfer conditions and then transfers asset state to buyer. +// TransferAsset can only be called by current owner +func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error { + clientOrgID, err := getClientOrgID(ctx, false) + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + transMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient data: %v", err) + } + + immutablePropertiesJSON, ok := transMap["asset_properties"] + if !ok { + return fmt.Errorf("asset_properties key not found in the transient map") + } + + priceJSON, ok := transMap["asset_price"] + if !ok { + return fmt.Errorf("asset_price key not found in the transient map") + } + + var agreement Agreement + err = json.Unmarshal(priceJSON, &agreement) + if err != nil { + return fmt.Errorf("failed to unmarshal price JSON: %v", err) + } + + asset, err := s.ReadAsset(ctx, assetID) + if err != nil { + return fmt.Errorf("failed to get asset: %v", err) + } + + err = verifyTransferConditions(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, priceJSON) + if err != nil { + return fmt.Errorf("failed transfer verification: %v", err) + } + + err = transferAssetState(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, agreement.Price) + if err != nil { + return fmt.Errorf("failed asset transfer: %v", err) + } + + return nil + +} + +// verifyTransferConditions checks that client org currently owns asset and that both parties have agreed on price +func verifyTransferConditions(ctx contractapi.TransactionContextInterface, + asset *Asset, + immutablePropertiesJSON []byte, + clientOrgID string, + buyerOrgID string, + priceJSON []byte) error { + + // CHECK1: Auth check to ensure that client's org actually owns the asset + + if clientOrgID != asset.OwnerOrg { + return fmt.Errorf("a client from %s cannot transfer a asset owned by %s", clientOrgID, asset.OwnerOrg) + } + + // CHECK2: Verify that the hash of the passed immutable properties matches the on-chain hash + + collectionSeller := buildCollectionName(clientOrgID) + immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID) + if err != nil { + return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err) + } + if immutablePropertiesOnChainHash == nil { + return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID) + } + + hash := sha256.New() + hash.Write(immutablePropertiesJSON) + calculatedPropertiesHash := hash.Sum(nil) + + // verify that the hash of the passed immutable properties matches the on-chain hash + if !bytes.Equal(immutablePropertiesOnChainHash, calculatedPropertiesHash) { + return fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x", + calculatedPropertiesHash, + immutablePropertiesJSON, + immutablePropertiesOnChainHash, + ) + } + + // CHECK3: Verify that seller and buyer agreed on the same price + + // Get sellers asking price + assetForSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + sellerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, assetForSaleKey) + if err != nil { + return fmt.Errorf("failed to get seller price hash: %v", err) + } + if sellerPriceHash == nil { + return fmt.Errorf("seller price for %s does not exist", asset.ID) + } + + // Get buyers bid price + collectionBuyer := buildCollectionName(buyerOrgID) + assetBidKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + buyerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, assetBidKey) + if err != nil { + return fmt.Errorf("failed to get buyer price hash: %v", err) + } + if buyerPriceHash == nil { + return fmt.Errorf("buyer price for %s does not exist", asset.ID) + } + + hash = sha256.New() + hash.Write(priceJSON) + calculatedPriceHash := hash.Sum(nil) + + // Verify that the hash of the passed price matches the on-chain sellers price hash + if !bytes.Equal(calculatedPriceHash, sellerPriceHash) { + return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, seller hasn't agreed to the passed trade id and price", + calculatedPriceHash, + priceJSON, + sellerPriceHash, + ) + } + + // Verify that the hash of the passed price matches the on-chain buyer price hash + if !bytes.Equal(calculatedPriceHash, buyerPriceHash) { + return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, buyer hasn't agreed to the passed trade id and price", + calculatedPriceHash, + priceJSON, + buyerPriceHash, + ) + } + + return nil +} + +// transferAssetState performs the public and private state updates for the transferred asset +func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, immutablePropertiesJSON []byte, clientOrgID string, buyerOrgID string, price int) error { + asset.OwnerOrg = buyerOrgID + updatedAsset, err := json.Marshal(asset) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(asset.ID, updatedAsset) + if err != nil { + return fmt.Errorf("failed to write asset for buyer: %v", err) + } + + // Change the endorsement policy to the new owner + err = setAssetStateBasedEndorsement(ctx, asset.ID, buyerOrgID) + if err != nil { + return fmt.Errorf("failed setting state based endorsement for new owner: %v", err) + } + + // Transfer the private properties (delete from seller collection, create in buyer collection) + collectionSeller := buildCollectionName(clientOrgID) + err = ctx.GetStub().DelPrivateData(collectionSeller, asset.ID) + if err != nil { + return fmt.Errorf("failed to delete Asset private details from seller: %v", err) + } + + collectionBuyer := buildCollectionName(buyerOrgID) + err = ctx.GetStub().PutPrivateData(collectionBuyer, asset.ID, immutablePropertiesJSON) + if err != nil { + return fmt.Errorf("failed to put Asset private properties for buyer: %v", err) + } + + // Delete the price records for seller + assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key for seller: %v", err) + } + + err = ctx.GetStub().DelPrivateData(collectionSeller, assetPriceKey) + if err != nil { + return fmt.Errorf("failed to delete asset price from implicit private data collection for seller: %v", err) + } + + // Delete the price records for buyer + assetPriceKey, err = ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key for buyer: %v", err) + } + + err = ctx.GetStub().DelPrivateData(collectionBuyer, assetPriceKey) + if err != nil { + return fmt.Errorf("failed to delete asset price from implicit private data collection for buyer: %v", err) + } + + // Keep record for a 'receipt' in both buyers and sellers private data collection to record the sale price and date. + // Persist the agreed to price in a collection sub-namespace based on receipt key prefix. + receiptBuyKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBuyReceipt, []string{asset.ID, ctx.GetStub().GetTxID()}) + if err != nil { + return fmt.Errorf("failed to create composite key for receipt: %v", err) + } + + txTimestamp, err := ctx.GetStub().GetTxTimestamp() + if err != nil { + return fmt.Errorf("failed to create timestamp for receipt: %v", err) + } + + timestamp, err := ptypes.Timestamp(txTimestamp) + if err != nil { + return err + } + assetReceipt := receipt{ + price: price, + timestamp: timestamp, + } + receipt, err := json.Marshal(assetReceipt) + if err != nil { + return fmt.Errorf("failed to marshal receipt: %v", err) + } + + err = ctx.GetStub().PutPrivateData(collectionBuyer, receiptBuyKey, receipt) + if err != nil { + return fmt.Errorf("failed to put private asset receipt for buyer: %v", err) + } + + receiptSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetSaleReceipt, []string{ctx.GetStub().GetTxID(), asset.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key for receipt: %v", err) + } + + err = ctx.GetStub().PutPrivateData(collectionSeller, receiptSaleKey, receipt) + if err != nil { + return fmt.Errorf("failed to put private asset receipt for seller: %v", err) + } + + return nil +} + +// getClientOrgID gets the client org ID. +// The client org ID can optionally be verified against the peer org ID, to ensure that a client +// from another org doesn't attempt to read or write private data from this peer. +// The only exception in this scenario is for TransferAsset, since the current owner +// needs to get an endorsement from the buyer's peer. +func getClientOrgID(ctx contractapi.TransactionContextInterface, verifyOrg bool) (string, error) { + clientOrgID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return "", fmt.Errorf("failed getting client's orgID: %v", err) + } + + if verifyOrg { + err = verifyClientOrgMatchesPeerOrg(clientOrgID) + if err != nil { + return "", err + } + } + + return clientOrgID, nil +} + +// verifyClientOrgMatchesPeerOrg checks the client org id matches the peer org id. +func verifyClientOrgMatchesPeerOrg(clientOrgID string) error { + peerOrgID, err := shim.GetMSPID() + if err != nil { + return fmt.Errorf("failed getting peer's orgID: %v", err) + } + + if clientOrgID != peerOrgID { + return fmt.Errorf("client from org %s is not authorized to read or write private data from an org %s peer", + clientOrgID, + peerOrgID, + ) + } + + return nil +} + +// setAssetStateBasedEndorsement adds an endorsement policy to a asset so that only a peer from an owning org +// can update or transfer the asset. +func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID 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(assetID, policy) + if err != nil { + return fmt.Errorf("failed to set validation parameter on asset: %v", err) + } + + return nil +} + +func buildCollectionName(clientOrgID string) string { + return fmt.Sprintf("_implicit_org_%s", clientOrgID) +} + +func getClientImplicitCollectionName(ctx contractapi.TransactionContextInterface) (string, error) { + clientOrgID, err := getClientOrgID(ctx, true) + if err != nil { + return "", fmt.Errorf("failed to get verified OrgID: %v", err) + } + + err = verifyClientOrgMatchesPeerOrg(clientOrgID) + if err != nil { + return "", err + } + + return buildCollectionName(clientOrgID), nil +} + +func main() { + chaincode, err := contractapi.NewChaincode(new(SmartContract)) + if err != nil { + log.Panicf("Error create transfer asset chaincode: %v", err) + } + + if err := chaincode.Start(); err != nil { + log.Panicf("Error starting asset chaincode: %v", err) + } +} diff --git a/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go b/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go new file mode 100644 index 0000000..7bc567f --- /dev/null +++ b/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go @@ -0,0 +1,176 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// QueryResult structure used for handling result of query +type QueryResult struct { + Record *Asset + TxId string `json:"txId"` + Timestamp time.Time `json:"timestamp"` +} + +type Agreement struct { + ID string `json:"asset_id"` + Price int `json:"price"` + TradeID string `json:"trade_id"` +} + +// ReadAsset returns the public asset data +func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) { + // Since only public data is accessed in this function, no access control is required + assetJSON, err := ctx.GetStub().GetState(assetID) + if err != nil { + return nil, fmt.Errorf("failed to read from world state: %v", err) + } + if assetJSON == nil { + return nil, fmt.Errorf("%s does not exist", assetID) + } + + var asset *Asset + err = json.Unmarshal(assetJSON, &asset) + if err != nil { + return nil, err + } + return asset, nil +} + +// GetAssetPrivateProperties returns the immutable asset properties from owner's private data collection +func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { + // In this scenario, client is only authorized to read/write private data from its own peer. + collection, err := getClientImplicitCollectionName(ctx) + if err != nil { + return "", err + } + + immutableProperties, err := ctx.GetStub().GetPrivateData(collection, assetID) + if err != nil { + return "", fmt.Errorf("failed to read asset private properties from client org's collection: %v", err) + } + if immutableProperties == nil { + return "", fmt.Errorf("asset private details does not exist in client org's collection: %s", assetID) + } + + return string(immutableProperties), nil +} + +// GetAssetSalesPrice returns the sales price +func (s *SmartContract) GetAssetSalesPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { + return getAssetPrice(ctx, assetID, typeAssetForSale) +} + +// GetAssetBidPrice returns the bid price +func (s *SmartContract) GetAssetBidPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { + return getAssetPrice(ctx, assetID, typeAssetBid) +} + +// getAssetPrice gets the bid or ask price from caller's implicit private data collection +func getAssetPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) (string, error) { + collection, err := getClientImplicitCollectionName(ctx) + if err != nil { + return "", err + } + + assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID}) + if err != nil { + return "", fmt.Errorf("failed to create composite key: %v", err) + } + + price, err := ctx.GetStub().GetPrivateData(collection, assetPriceKey) + if err != nil { + return "", fmt.Errorf("failed to read asset price from implicit private data collection: %v", err) + } + if price == nil { + return "", fmt.Errorf("asset price does not exist: %s", assetID) + } + + return string(price), nil +} + +// QueryAssetSaleAgreements returns all of an organization's proposed sales +func (s *SmartContract) QueryAssetSaleAgreements(ctx contractapi.TransactionContextInterface) ([]Agreement, error) { + return queryAgreementsByType(ctx, typeAssetForSale) +} + +// QueryAssetBuyAgreements returns all of an organization's proposed bids +func (s *SmartContract) QueryAssetBuyAgreements(ctx contractapi.TransactionContextInterface) ([]Agreement, error) { + return queryAgreementsByType(ctx, typeAssetBid) +} + +func queryAgreementsByType(ctx contractapi.TransactionContextInterface, agreeType string) ([]Agreement, error) { + collection, err := getClientImplicitCollectionName(ctx) + if err != nil { + return nil, err + } + + // Query for any object type starting with `agreeType` + agreementsIterator, err := ctx.GetStub().GetPrivateDataByPartialCompositeKey(collection, agreeType, []string{}) + if err != nil { + return nil, fmt.Errorf("failed to read from private data collection: %v", err) + } + defer agreementsIterator.Close() + + var agreements []Agreement + for agreementsIterator.HasNext() { + resp, err := agreementsIterator.Next() + if err != nil { + return nil, err + } + + var agreement Agreement + err = json.Unmarshal(resp.Value, &agreement) + if err != nil { + return nil, err + } + + agreements = append(agreements, agreement) + } + + return agreements, nil +} + +// QueryAssetHistory returns the chain of custody for a asset since issuance +func (s *SmartContract) QueryAssetHistory(ctx contractapi.TransactionContextInterface, assetID string) ([]QueryResult, error) { + resultsIterator, err := ctx.GetStub().GetHistoryForKey(assetID) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + var results []QueryResult + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + var asset *Asset + err = json.Unmarshal(response.Value, &asset) + if err != nil { + return nil, err + } + + timestamp, err := ptypes.Timestamp(response.Timestamp) + if err != nil { + return nil, err + } + record := QueryResult{ + TxId: response.TxId, + Timestamp: timestamp, + Record: asset, + } + results = append(results, record) + } + + return results, nil +} diff --git a/asset-transfer-secured-agreement/chaincode-go/go.mod b/asset-transfer-secured-agreement/chaincode-go/go.mod new file mode 100644 index 0000000..4f36be9 --- /dev/null +++ b/asset-transfer-secured-agreement/chaincode-go/go.mod @@ -0,0 +1,9 @@ +module github.com/hyperledger/fabric-samples/chaincode/tradingMarbles + +go 1.14 + +require ( + github.com/golang/protobuf v1.3.2 + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200128192331-2d899240a7ed + github.com/hyperledger/fabric-contract-api-go v1.0.0 +) diff --git a/asset-transfer-secured-agreement/chaincode-go/go.sum b/asset-transfer-secured-agreement/chaincode-go/go.sum new file mode 100644 index 0000000..6d9ad15 --- /dev/null +++ b/asset-transfer-secured-agreement/chaincode-go/go.sum @@ -0,0 +1,136 @@ +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/godog v0.7.13/go.mod h1:z2OZ6a3X0/YAKVqLfVzYBwFt3j6uSt3Xrqa7XTtcQE0= +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/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/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/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +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-20200128192331-2d899240a7ed h1:VNnrD/ilIUO9DDHQP/uioYSy1309rYy0Z1jf3GLNRIc= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200128192331-2d899240a7ed/go.mod h1:N7H3sA7Tx4k/YzFq7U0EPdqJtqvM4Kild0JoCc7C0Dc= +github.com/hyperledger/fabric-contract-api-go v1.0.0 h1:ma1nQX1S/a3zDkfkTb0QXQHNGgJUmEfqHA9/CWmz8Y0= +github.com/hyperledger/fabric-contract-api-go v1.0.0/go.mod h1:PHF7I0hYI0cZF2j7cdyNHaY5FJD3Q49qnnNgsmxEPbM= +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-20200124220212-e9cfc186ba7b h1:rZ3Vro68vStzLYfcSrQlprjjCf5UmFk7QjKGgHL8IQg= +github.com/hyperledger/fabric-protos-go v0.0.0-20200124220212-e9cfc186ba7b/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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/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/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-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/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/README.md b/auction/README.md new file mode 100644 index 0000000..7389172 --- /dev/null +++ b/auction/README.md @@ -0,0 +1,416 @@ +## Simple blind auction sample + +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 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. + +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 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 + +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/ -ccl 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 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 PaintingAuction painting +``` + +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", + "organizations": [ + "Org1MSP" + ], + "privateBids": {}, + "revealedBids": {}, + "winner": "", + "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 PaintingAuction 800 +``` + +The application will query the bid after it is created: +``` +*** Result: Bid: { + "objectType": "bid", + "price": 800, + "org": "Org1MSP", + "bidder": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" +} +``` + +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: 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=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 PaintingAuction $BIDDER1_BID_ID +``` + +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", + "organizations": [ + "Org1MSP" + ], + "privateBids": { + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { + "org": "Org1MSP", + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" + } + }, + "revealedBids": {}, + "winner": "", + "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 PaintingAuction 500 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER2_BID_ID=915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185 +``` + +Submit bidder2's bid to the auction: +``` +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 PaintingAuction 700 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER3_BID_ID=5e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad +``` + +Add bidder3's bid to the auction: +``` +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: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "painting", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000PaintingAuction\u00005e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad\u0000": { + "org": "Org2MSP", + "hash": "40107eab7a99dfc2f25d02b8ab840f12fd802a9f86d8d42b78d7b4409b2c15bd" + }, + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { + "org": "Org1MSP", + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" + }, + "\u0000bid\u0000PaintingAuction\u0000915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185\u0000": { + "org": "Org1MSP", + "hash": "a458df18b12dffe4ae6d56a270134c2d55bd53fface034bd24381d0073d46a45" + } + }, + "revealedBids": {}, + "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 `"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 PaintingAuction 900 +``` + +Save the Bid ID returned by the application: +``` +export BIDDER4_BID_ID=49466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35 +``` + +Add bidder4's bid to the auction: +``` +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 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. + +## 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 PaintingAuction $BIDDER1_BID_ID +``` + +The full bid details, including the price, are now visible: +``` +*** Result: Auction: { + "objectType": "auction", + "item": "painting", + "seller": "eDUwOTo6Q049c2VsbGVyLE9VPWNsaWVudCtPVT1vcmcxK09VPWRlcGFydG1lbnQxOjpDTj1jYS5vcmcxLmV4YW1wbGUuY29tLE89b3JnMS5leGFtcGxlLmNvbSxMPUR1cmhhbSxTVD1Ob3J0aCBDYXJvbGluYSxDPVVT", + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000PaintingAuction\u000049466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35\u0000": { + "org": "Org2MSP", + "hash": "b8eaeb4422b93abdfe4ccb6aa11b745b3d1cb072a99bd3eb3618f081fb1b1f89" + }, + "\u0000bid\u0000PaintingAuction\u00005e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad\u0000": { + "org": "Org2MSP", + "hash": "40107eab7a99dfc2f25d02b8ab840f12fd802a9f86d8d42b78d7b4409b2c15bd" + }, + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { + "org": "Org1MSP", + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" + }, + "\u0000bid\u0000PaintingAuction\u0000915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185\u0000": { + "org": "Org1MSP", + "hash": "a458df18b12dffe4ae6d56a270134c2d55bd53fface034bd24381d0073d46a45" + } + }, + "revealedBids": { + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { + "objectType": "bid", + "price": 800, + "org": "Org1MSP", + "bidder": "eDUwOTo6Q049YmlkZGVyMSxPVT1jbGllbnQrT1U9b3JnMStPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw==" + } + }, + "winner": "", + "price": 0, + "status": "closed" +} +``` + +Bidder3 from Org2 will also reveal their bid: +``` +node revealBid.js org2 bidder3 PaintingAuction $BIDDER3_BID_ID +``` + +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 PaintingAuction +``` + +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 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. + +## End the auction + +Now that the winning bids have been revealed, we can end the auction: +``` +node endAuction org1 seller PaintingAuction +``` + +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", + "organizations": [ + "Org1MSP", + "Org2MSP" + ], + "privateBids": { + "\u0000bid\u0000PaintingAuction\u000049466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35\u0000": { + "org": "Org2MSP", + "hash": "b8eaeb4422b93abdfe4ccb6aa11b745b3d1cb072a99bd3eb3618f081fb1b1f89" + }, + "\u0000bid\u0000PaintingAuction\u00005e4e637c68833b178739575f6fe09820b019551a8cfbb43a4d172e0aa864dfad\u0000": { + "org": "Org2MSP", + "hash": "40107eab7a99dfc2f25d02b8ab840f12fd802a9f86d8d42b78d7b4409b2c15bd" + }, + "\u0000bid\u0000PaintingAuction\u00008ef83011a5fb791f75ed008337839426f6b87981519e5d58ef5ada39c3044edd\u0000": { + "org": "Org1MSP", + "hash": "5cb50a17b5a21c02fc01306e3e9b54f4db67e9a440552ce898bbd7daa62dce0f" + }, + "\u0000bid\u0000PaintingAuction\u0000915a908c8f2c368f4a3aedd73176656af81ddfab000b11629503403f3d97b185\u0000": { + "org": "Org1MSP", + "hash": "a458df18b12dffe4ae6d56a270134c2d55bd53fface034bd24381d0073d46a45" + } + }, + "revealedBids": { + "\u0000bid\u0000PaintingAuction\u000049466271ae879bd009e75a60730a12bfa986e75f263202ab81ccd3deec544a35\u0000": { + "objectType": "bid", + "price": 900, + "org": "Org2MSP", + "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==" + } + }, + "winner": "eDUwOTo6Q049YmlkZGVyNCxPVT1jbGllbnQrT1U9b3JnMitPVT1kZXBhcnRtZW50MTo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL", + "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 0000000..a5540d8 --- /dev/null +++ b/auction/application-javascript/bid.js @@ -0,0 +1,112 @@ +/* + * 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,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 bidder = await contract.evaluateTransaction('GetID'); + console.log('*** Result: Bidder ID is ' + bidder.toString()); + + let bidData = { objectType: 'bid', price: parseInt(price), org: orgMSP, bidder: bidder.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 organization\'s private data collection'); + 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) { + 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 price = 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 bid(ccp,wallet,user,orgMSP,auctionID,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,price); + } else { + console.log("Usage: node bid.js org userID auctionID 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 0000000..7934fbf --- /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 0000000..8d083d7 --- /dev/null +++ b/auction/application-javascript/createAuction.js @@ -0,0 +1,93 @@ +/* + * 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) { + 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); + 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) { + console.log("Usage: node createAuction.js org userID auctionID item"); + process.exit(1); + } + + const org = process.argv[2] + const user = process.argv[3]; + const auctionID = process.argv[4]; + const item = 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 createAuction(ccp,wallet,user,auctionID,item); + } + 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); + } else { + console.log("Usage: node createAuction.js org userID auctionID item"); + 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 0000000..3c16c7f --- /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 0000000..e2df9cf --- /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 0000000..4a410a6 --- /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.4", + "fabric-network": "^2.2.4" + } +} diff --git a/auction/application-javascript/queryAuction.js b/auction/application-javascript/queryAuction.js new file mode 100644 index 0000000..258d19c --- /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 0000000..b4ffea6 --- /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 0000000..3ee97a6 --- /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 0000000..8b39743 --- /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', price: parseInt(bidJSON.price), org: bidJSON.org, bidder: bidJSON.bidder}; + 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 0000000..ef6cf2a --- /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 0000000..26f43af --- /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 0000000..f052c52 --- /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 0000000..c197e7e --- /dev/null +++ b/auction/chaincode-go/smart-contract/auction.go @@ -0,0 +1,487 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package auction + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + + "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"` + Orgs []string `json:"organizations"` + PrivateBids map[string]BidHash `json:"privateBids"` + RevealedBids map[string]FullBid `json:"revealedBids"` + Winner string `json:"winner"` + Price int `json:"price"` + Status string `json:"status"` +} + +// FullBid is the structure of a revealed bid +type FullBid struct { + Type string `json:"objectType"` + Price int `json:"price"` + Org string `json:"org"` + Bidder string `json:"bidder"` +} + +// BidHash is the structure of a private bid +type BidHash struct { + Org string `json:"org"` + Hash string `json:"hash"` +} + +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) 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, + Price: 0, + Seller: clientID, + Orgs: []string{clientOrgID}, + PrivateBids: bidders, + RevealedBids: revealedBids, + Winner: "", + 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 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) { + + // 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 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) + } + 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 organizations 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 a + // 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 { + Price int `json:"price"` + Org string `json:"org"` + Bidder string `json:"bidder"` + } + + // 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, + Price: bidInput.Price, + Org: bidInput.Org, + Bidder: bidInput.Bidder, + } + + // check 4: make sure that the transaction is being submitted is the bidder + if bidInput.Bidder != 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) + } + + // determine the highest bid + for _, bid := range revealedBidMap { + if bid.Price > auctionJSON.Price { + auctionJSON.Winner = bid.Bidder + auctionJSON.Price = bid.Price + } + } + + // 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 0000000..64eb45b --- /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.Bidder != 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 0000000..8c30524 --- /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 0000000..8aa5637 --- /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/chaincode/README.md b/chaincode/README.md new file mode 100644 index 0000000..510528c --- /dev/null +++ b/chaincode/README.md @@ -0,0 +1,20 @@ +[//]: # (SPDX-License-Identifier: CC-BY-4.0) + +# Sample smart contracts + +This folder contains example smart contracts. It is recommended that users start with the Asset transfer samples and tutorials series for the most recent example smart contracts. + +| **Smart Contract** | **Description** | **Languages** | +| -----------|------------------------------|---------| +| [fabcar](fabcar) | Basic smart contract that allows you to add and change data on the ledger using the Fabric contract API. Also contains an example on how to run chaincode as an external service. | Go, Java, JavaScript, Typescript | +| [marbles02](marbles02) | Sample that demonstrates how to deploy an index and use rich queries when you are using CouchDB as your state database. | Go | +| [marbles02_private](marbles02_private) | Sample that demonstrates the use of private data collections. | Go | +| [sacc](sacc) | Simple asset chaincode that interacts with the ledger using the low-level APIs provided by the Fabric Chaincode Shim API. | Go | +| [abstore](abstore) | Basic smart contract that allows you to transfer data (from A to B) using the Fabric contract API. | Go, Java, JavaScript | + +## License + +Hyperledger Project source code files are made available under the Apache +License, Version 2.0 (Apache-2.0), located in the [LICENSE](LICENSE) file. +Hyperledger Project documentation files are made available under the Creative +Commons Attribution 4.0 International License (CC-BY-4.0), available at http://creativecommons.org/licenses/by/4.0/. diff --git a/chaincode/abstore/go/abstore.go b/chaincode/abstore/go/abstore.go new file mode 100644 index 0000000..9b35a22 --- /dev/null +++ b/chaincode/abstore/go/abstore.go @@ -0,0 +1,135 @@ +/* +Copyright IBM Corp. 2016 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "strconv" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// ABstore Chaincode implementation +type ABstore struct { + contractapi.Contract +} + +func (t *ABstore) Init(ctx contractapi.TransactionContextInterface, A string, Aval int, B string, Bval int) error { + fmt.Println("ABstore Init") + var err error + // Initialize the chaincode + fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval) + // Write the state to the ledger + err = ctx.GetStub().PutState(A, []byte(strconv.Itoa(Aval))) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(B, []byte(strconv.Itoa(Bval))) + if err != nil { + return err + } + + return nil +} + +// Transaction makes payment of X units from A to B +func (t *ABstore) Invoke(ctx contractapi.TransactionContextInterface, A, B string, X int) error { + var err error + var Aval int + var Bval int + // Get the state from the ledger + // TODO: will be nice to have a GetAllState call to ledger + Avalbytes, err := ctx.GetStub().GetState(A) + if err != nil { + return fmt.Errorf("Failed to get state") + } + if Avalbytes == nil { + return fmt.Errorf("Entity not found") + } + Aval, _ = strconv.Atoi(string(Avalbytes)) + + Bvalbytes, err := ctx.GetStub().GetState(B) + if err != nil { + return fmt.Errorf("Failed to get state") + } + if Bvalbytes == nil { + return fmt.Errorf("Entity not found") + } + Bval, _ = strconv.Atoi(string(Bvalbytes)) + + // Perform the execution + Aval = Aval - X + Bval = Bval + X + fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval) + + // Write the state back to the ledger + err = ctx.GetStub().PutState(A, []byte(strconv.Itoa(Aval))) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(B, []byte(strconv.Itoa(Bval))) + if err != nil { + return err + } + + return nil +} + +// Delete an entity from state +func (t *ABstore) Delete(ctx contractapi.TransactionContextInterface, A string) error { + + // Delete the key from the state in ledger + err := ctx.GetStub().DelState(A) + if err != nil { + return fmt.Errorf("Failed to delete state") + } + + return nil +} + +// Query callback representing the query of a chaincode +func (t *ABstore) Query(ctx contractapi.TransactionContextInterface, A string) (string, error) { + var err error + // Get the state from the ledger + Avalbytes, err := ctx.GetStub().GetState(A) + if err != nil { + jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}" + return "", errors.New(jsonResp) + } + + if Avalbytes == nil { + jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}" + return "", errors.New(jsonResp) + } + + jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}" + fmt.Printf("Query Response:%s\n", jsonResp) + return string(Avalbytes), nil +} + +func main() { + cc, err := contractapi.NewChaincode(new(ABstore)) + if err != nil { + panic(err.Error()) + } + if err := cc.Start(); err != nil { + fmt.Printf("Error starting ABstore chaincode: %s", err) + } +} diff --git a/chaincode/abstore/go/go.mod b/chaincode/abstore/go/go.mod new file mode 100644 index 0000000..3891885 --- /dev/null +++ b/chaincode/abstore/go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/chaincode/abstore/go + +go 1.13 + +require github.com/hyperledger/fabric-contract-api-go v1.1.0 diff --git a/chaincode/abstore/go/go.sum b/chaincode/abstore/go/go.sum new file mode 100644 index 0000000..12dd961 --- /dev/null +++ b/chaincode/abstore/go/go.sum @@ -0,0 +1,149 @@ +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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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/chaincode/abstore/java/.gitignore b/chaincode/abstore/java/.gitignore new file mode 100644 index 0000000..7005557 --- /dev/null +++ b/chaincode/abstore/java/.gitignore @@ -0,0 +1,61 @@ + +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Gradle +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +# Eclipse files +.project +.classpath +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.externalToolBuilders/ +*.launch diff --git a/chaincode/abstore/java/build.gradle b/chaincode/abstore/java/build.gradle new file mode 100644 index 0000000..aec70af --- /dev/null +++ b/chaincode/abstore/java/build.gradle @@ -0,0 +1,39 @@ +/* + * Copyright IBM Corp. 2018 All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +plugins { + id 'com.github.johnrengelman.shadow' version '5.1.0' + id 'java' +} + +group 'org.hyperledger.fabric-chaincode-java' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.8 + +repositories { + mavenLocal() + mavenCentral() + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + maven { + url 'https://jitpack.io' + } +} + +dependencies { + implementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' +} + +shadowJar { + baseName = 'chaincode' + version = null + classifier = null + + manifest { + attributes 'Main-Class': 'org.hyperledger.fabric_samples.ABstore' + } +} diff --git a/chaincode/abstore/java/gradle/wrapper/gradle-wrapper.jar b/chaincode/abstore/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/chaincode/abstore/java/gradlew.bat b/chaincode/abstore/java/gradlew.bat new file mode 100644 index 0000000..24467a1 --- /dev/null +++ b/chaincode/abstore/java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/chaincode/abstore/java/settings.gradle b/chaincode/abstore/java/settings.gradle new file mode 100644 index 0000000..80cbe43 --- /dev/null +++ b/chaincode/abstore/java/settings.gradle @@ -0,0 +1,7 @@ +/* + * Copyright IBM Corp. 2017 All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +rootProject.name = 'abstore' + diff --git a/chaincode/abstore/java/src/main/java/org/hyperledger/fabric-samples/ABstore.java b/chaincode/abstore/java/src/main/java/org/hyperledger/fabric-samples/ABstore.java new file mode 100644 index 0000000..634895d --- /dev/null +++ b/chaincode/abstore/java/src/main/java/org/hyperledger/fabric-samples/ABstore.java @@ -0,0 +1,136 @@ +/* +Copyright IBM Corp., DTCC All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +package org.hyperledger.fabric_samples; + +import java.util.List; + +import com.google.protobuf.ByteString; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hyperledger.fabric.shim.ChaincodeBase; +import org.hyperledger.fabric.shim.ChaincodeStub; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ABstore extends ChaincodeBase { + + private static Log _logger = LogFactory.getLog(ABstore.class); + + @Override + public Response init(ChaincodeStub stub) { + try { + _logger.info("Init java simple chaincode"); + List args = stub.getParameters(); + if (args.size() != 4) { + newErrorResponse("Incorrect number of arguments. Expecting 4"); + } + // Initialize the chaincode + String account1Key = args.get(0); + int account1Value = Integer.parseInt(args.get(1)); + String account2Key = args.get(2); + int account2Value = Integer.parseInt(args.get(3)); + + _logger.info(String.format("account %s, value = %s; account %s, value %s", account1Key, account1Value, account2Key, account2Value)); + stub.putStringState(account1Key, args.get(1)); + stub.putStringState(account2Key, args.get(3)); + + return newSuccessResponse(); + } catch (Throwable e) { + return newErrorResponse(e); + } + } + + @Override + public Response invoke(ChaincodeStub stub) { + try { + _logger.info("Invoke java simple chaincode"); + String func = stub.getFunction(); + List params = stub.getParameters(); + if (func.equals("invoke")) { + return invoke(stub, params); + } + if (func.equals("delete")) { + return delete(stub, params); + } + if (func.equals("query")) { + return query(stub, params); + } + return newErrorResponse("Invalid invoke function name. Expecting one of: [\"invoke\", \"delete\", \"query\"]"); + } catch (Throwable e) { + return newErrorResponse(e); + } + } + + private Response invoke(ChaincodeStub stub, List args) { + if (args.size() != 3) { + return newErrorResponse("Incorrect number of arguments. Expecting 3"); + } + String accountFromKey = args.get(0); + String accountToKey = args.get(1); + + String accountFromValueStr = stub.getStringState(accountFromKey); + if (accountFromValueStr == null) { + return newErrorResponse(String.format("Entity %s not found", accountFromKey)); + } + int accountFromValue = Integer.parseInt(accountFromValueStr); + + String accountToValueStr = stub.getStringState(accountToKey); + if (accountToValueStr == null) { + return newErrorResponse(String.format("Entity %s not found", accountToKey)); + } + int accountToValue = Integer.parseInt(accountToValueStr); + + int amount = Integer.parseInt(args.get(2)); + + if (amount > accountFromValue) { + return newErrorResponse(String.format("not enough money in account %s", accountFromKey)); + } + + accountFromValue -= amount; + accountToValue += amount; + + _logger.info(String.format("new value of A: %s", accountFromValue)); + _logger.info(String.format("new value of B: %s", accountToValue)); + + stub.putStringState(accountFromKey, Integer.toString(accountFromValue)); + stub.putStringState(accountToKey, Integer.toString(accountToValue)); + + _logger.info("Transfer complete"); + + return newSuccessResponse("invoke finished successfully", ByteString.copyFrom(accountFromKey + ": " + accountFromValue + " " + accountToKey + ": " + accountToValue, UTF_8).toByteArray()); + } + + // Deletes an entity from state + private Response delete(ChaincodeStub stub, List args) { + if (args.size() != 1) { + return newErrorResponse("Incorrect number of arguments. Expecting 1"); + } + String key = args.get(0); + // Delete the key from the state in ledger + stub.delState(key); + return newSuccessResponse(); + } + + // query callback representing the query of a chaincode + private Response query(ChaincodeStub stub, List args) { + if (args.size() != 1) { + return newErrorResponse("Incorrect number of arguments. Expecting name of the person to query"); + } + String key = args.get(0); + //byte[] stateBytes + String val = stub.getStringState(key); + if (val == null) { + return newErrorResponse(String.format("Error: state for %s is null", key)); + } + _logger.info(String.format("Query Response:\nName: %s, Amount: %s\n", key, val)); + return newSuccessResponse(val, ByteString.copyFrom(val, UTF_8).toByteArray()); + } + + public static void main(String[] args) { + new ABstore().start(args); + } + +} diff --git a/chaincode/abstore/javascript/.gitignore b/chaincode/abstore/javascript/.gitignore new file mode 100644 index 0000000..a00ca94 --- /dev/null +++ b/chaincode/abstore/javascript/.gitignore @@ -0,0 +1,77 @@ +# +# 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/ + +# 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/chaincode/abstore/javascript/abstore.js b/chaincode/abstore/javascript/abstore.js new file mode 100644 index 0000000..adcfdd9 --- /dev/null +++ b/chaincode/abstore/javascript/abstore.js @@ -0,0 +1,138 @@ +/* +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +const shim = require('fabric-shim'); +const util = require('util'); + +var ABstore = class { + + // Initialize the chaincode + async Init(stub) { + console.info('========= ABstore Init ========='); + let ret = stub.getFunctionAndParameters(); + console.info(ret); + let args = ret.params; + // initialise only if 4 parameters passed. + if (args.length != 4) { + return shim.error('Incorrect number of arguments. Expecting 4'); + } + + let A = args[0]; + let B = args[2]; + let Aval = args[1]; + let Bval = args[3]; + + if (typeof parseInt(Aval) !== 'number' || typeof parseInt(Bval) !== 'number') { + return shim.error('Expecting integer value for asset holding'); + } + + try { + await stub.putState(A, Buffer.from(Aval)); + try { + await stub.putState(B, Buffer.from(Bval)); + return shim.success(); + } catch (err) { + return shim.error(err); + } + } catch (err) { + return shim.error(err); + } + } + + async Invoke(stub) { + let ret = stub.getFunctionAndParameters(); + console.info(ret); + let method = this[ret.fcn]; + if (!method) { + console.log('no method of name:' + ret.fcn + ' found'); + return shim.success(); + } + try { + let payload = await method(stub, ret.params); + return shim.success(payload); + } catch (err) { + console.log(err); + return shim.error(err); + } + } + + async invoke(stub, args) { + if (args.length != 3) { + throw new Error('Incorrect number of arguments. Expecting 3'); + } + + let A = args[0]; + let B = args[1]; + if (!A || !B) { + throw new Error('asset holding must not be empty'); + } + + // Get the state from the ledger + let Avalbytes = await stub.getState(A); + if (!Avalbytes) { + throw new Error('Failed to get state of asset holder A'); + } + let Aval = parseInt(Avalbytes.toString()); + + let Bvalbytes = await stub.getState(B); + if (!Bvalbytes) { + throw new Error('Failed to get state of asset holder B'); + } + + let Bval = parseInt(Bvalbytes.toString()); + // Perform the execution + let amount = parseInt(args[2]); + if (typeof amount !== 'number') { + throw new Error('Expecting integer value for amount to be transaferred'); + } + + Aval = Aval - amount; + Bval = Bval + amount; + console.info(util.format('Aval = %d, Bval = %d\n', Aval, Bval)); + + // Write the states back to the ledger + await stub.putState(A, Buffer.from(Aval.toString())); + await stub.putState(B, Buffer.from(Bval.toString())); + + } + + // Deletes an entity from state + async delete(stub, args) { + if (args.length != 1) { + throw new Error('Incorrect number of arguments. Expecting 1'); + } + + let A = args[0]; + + // Delete the key from the state in ledger + await stub.deleteState(A); + } + + // query callback representing the query of a chaincode + async query(stub, args) { + if (args.length != 1) { + throw new Error('Incorrect number of arguments. Expecting name of the person to query') + } + + let jsonResp = {}; + let A = args[0]; + + // Get the state from the ledger + let Avalbytes = await stub.getState(A); + if (!Avalbytes) { + jsonResp.error = 'Failed to get state for ' + A; + throw new Error(JSON.stringify(jsonResp)); + } + + jsonResp.name = A; + jsonResp.amount = Avalbytes.toString(); + console.info('Query Response:'); + console.info(jsonResp); + return Avalbytes; + } +}; + +shim.start(new ABstore()); diff --git a/chaincode/abstore/javascript/package.json b/chaincode/abstore/javascript/package.json new file mode 100644 index 0000000..74dc19c --- /dev/null +++ b/chaincode/abstore/javascript/package.json @@ -0,0 +1,17 @@ +{ + "name": "abstore", + "version": "1.0.0", + "description": "ABstore chaincode implemented in node.js", + "engines": { + "node": ">=8.4.0", + "npm": ">=5.3.0" + }, + "scripts": { + "start": "node abstore.js" + }, + "engine-strict": true, + "license": "Apache-2.0", + "dependencies": { + "fabric-shim": "^2.0.0" + } +} diff --git a/chaincode/fabcar/external/.dockerignore b/chaincode/fabcar/external/.dockerignore new file mode 100644 index 0000000..61260e5 --- /dev/null +++ b/chaincode/fabcar/external/.dockerignore @@ -0,0 +1,5 @@ +chaincode.env* +*.json +*.md +*.tar.gz +*.tgz diff --git a/chaincode/fabcar/external/.gitignore b/chaincode/fabcar/external/.gitignore new file mode 100644 index 0000000..3f4b4e1 --- /dev/null +++ b/chaincode/fabcar/external/.gitignore @@ -0,0 +1,4 @@ +chaincode.env +connection.json +*.tar.gz +*.tgz \ No newline at end of file diff --git a/chaincode/fabcar/external/Dockerfile b/chaincode/fabcar/external/Dockerfile new file mode 100644 index 0000000..ef427a8 --- /dev/null +++ b/chaincode/fabcar/external/Dockerfile @@ -0,0 +1,17 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +ARG GO_VER=1.13.8 +ARG ALPINE_VER=3.10 + +FROM golang:${GO_VER}-alpine${ALPINE_VER} + +WORKDIR /go/src/github.com/hyperledger/fabric-samples/chaincode/fabcar/external +COPY . . + +RUN go get -d -v ./... +RUN go install -v ./... + +EXPOSE 9999 +CMD ["external"] diff --git a/chaincode/fabcar/external/README.md b/chaincode/fabcar/external/README.md new file mode 100644 index 0000000..6d2954e --- /dev/null +++ b/chaincode/fabcar/external/README.md @@ -0,0 +1,55 @@ +# FabCar as an external service + +See the "Chaincode as an external service" documentation for running chaincode as an external service. +This includes details of the external builder and launcher scripts which will peers in your Fabric network will require. + +The FabCar chaincode requires two environment variables to run, `CHAINCODE_SERVER_ADDRESS` and `CHAINCODE_ID`, which are described in the `chaincode.env.example` file. Copy this file to `chaincode.env` before continuing. + +**Note:** each organization in a Fabric network will need to follow the instructions below to host their own instance of the FabCar external service. + +## Packaging and installing + +Make sure the value of `CHAINCODE_SERVER_ADDRESS` in `chaincode.env` is correct for the FabCar external service you will be running. + +The peer needs a `connection.json` configuration file so that it can connect to the external FabCar service. +Use the `CHAINCODE_SERVER_ADDRESS` value in `chaincode.env` to create the `connection.json` file with the following command (requires [jq](https://stedolan.github.io/jq/)): + +``` +env $(cat chaincode.env | grep -v "#" | xargs) jq -n '{"address":env.CHAINCODE_SERVER_ADDRESS,"dial_timeout": "10s","tls_required": false}' > connection.json +``` + +Add this file to a `code.tar.gz` archive ready for adding to a FabCar external service package: + +``` +tar cfz code.tar.gz connection.json +``` + +Package the FabCar external service using the supplied `metadata.json` file: + +``` +tar cfz fabcar-pkg.tgz metadata.json code.tar.gz +``` + +Install the `fabcar-pkg.tgz` chaincode as usual, for example: + +``` +peer lifecycle chaincode install ./fabcar-pkg.tgz +``` + +## Running the FabCar external service + +To run the service in a container, build a FabCar docker image: + +``` +docker build -t hyperledger/fabcar-sample . +``` + +Edit the `chaincode.env` file to configure the `CHAINCODE_ID` variable before starting a FabCar container using the following command: + +``` +docker run -it --rm --name fabcar.org1.example.com --hostname fabcar.org1.example.com --env-file chaincode.env --network=net_test hyperledger/fabcar-sample +``` + +## Starting the FabCar external service + +Complete the remaining lifecycle steps to start the FabCar chaincode! diff --git a/chaincode/fabcar/external/chaincode.env.example b/chaincode/fabcar/external/chaincode.env.example new file mode 100644 index 0000000..ea3ba52 --- /dev/null +++ b/chaincode/fabcar/external/chaincode.env.example @@ -0,0 +1,8 @@ +# CHAINCODE_SERVER_ADDRESS must be set to the host and port where the peer can +# connect to the chaincode server +CHAINCODE_SERVER_ADDRESS=fabcar.org1.example.com:9999 + +# CHAINCODE_ID must be set to the Package ID that is assigned to the chaincode +# on install. The `peer lifecycle chaincode queryinstalled` command can be +# used to get the ID after install if required +CHAINCODE_ID=fabcar:... diff --git a/chaincode/fabcar/external/fabcar.go b/chaincode/fabcar/external/fabcar.go new file mode 100644 index 0000000..c48211d --- /dev/null +++ b/chaincode/fabcar/external/fabcar.go @@ -0,0 +1,172 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +type ServerConfig struct { + CCID string + Address string +} + +// SmartContract provides functions for managing a car +type SmartContract struct { + contractapi.Contract +} + +// Car describes basic details of what makes up a car +type Car struct { + Make string `json:"make"` + Model string `json:"model"` + Colour string `json:"colour"` + Owner string `json:"owner"` +} + +// QueryResult structure used for handling result of query +type QueryResult struct { + Key string `json:"Key"` + Record *Car +} + +// InitLedger adds a base set of cars to the ledger +func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { + cars := []Car{ + Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"}, + Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"}, + Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"}, + Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"}, + Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"}, + Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"}, + Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"}, + Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"}, + Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"}, + Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"}, + } + + for i, car := range cars { + carAsBytes, _ := json.Marshal(car) + err := ctx.GetStub().PutState("CAR"+strconv.Itoa(i), carAsBytes) + + if err != nil { + return fmt.Errorf("Failed to put to world state. %s", err.Error()) + } + } + + return nil +} + +// CreateCar adds a new car to the world state with given details +func (s *SmartContract) CreateCar(ctx contractapi.TransactionContextInterface, carNumber string, make string, model string, colour string, owner string) error { + car := Car{ + Make: make, + Model: model, + Colour: colour, + Owner: owner, + } + + carAsBytes, _ := json.Marshal(car) + + return ctx.GetStub().PutState(carNumber, carAsBytes) +} + +// QueryCar returns the car stored in the world state with given id +func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) { + carAsBytes, err := ctx.GetStub().GetState(carNumber) + + if err != nil { + return nil, fmt.Errorf("Failed to read from world state. %s", err.Error()) + } + + if carAsBytes == nil { + return nil, fmt.Errorf("%s does not exist", carNumber) + } + + car := new(Car) + _ = json.Unmarshal(carAsBytes, car) + + return car, nil +} + +// QueryAllCars returns all cars found in world state +func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) { + startKey := "" + endKey := "" + + resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey) + + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []QueryResult{} + + for resultsIterator.HasNext() { + queryResponse, err := resultsIterator.Next() + + if err != nil { + return nil, err + } + + car := new(Car) + _ = json.Unmarshal(queryResponse.Value, car) + + queryResult := QueryResult{Key: queryResponse.Key, Record: car} + results = append(results, queryResult) + } + + return results, nil +} + +// ChangeCarOwner updates the owner field of car with given id in world state +func (s *SmartContract) ChangeCarOwner(ctx contractapi.TransactionContextInterface, carNumber string, newOwner string) error { + car, err := s.QueryCar(ctx, carNumber) + + if err != nil { + return err + } + + car.Owner = newOwner + + carAsBytes, _ := json.Marshal(car) + + return ctx.GetStub().PutState(carNumber, carAsBytes) +} + +func main() { + // See chaincode.env.example + config := ServerConfig{ + CCID: os.Getenv("CHAINCODE_ID"), + Address: os.Getenv("CHAINCODE_SERVER_ADDRESS"), + } + + chaincode, err := contractapi.NewChaincode(new(SmartContract)) + + if err != nil { + fmt.Printf("Error create fabcar chaincode: %s", err.Error()) + return + } + + server := &shim.ChaincodeServer{ + CCID: config.CCID, + Address: config.Address, + CC: chaincode, + TLSProps: shim.TLSProperties{ + Disabled: true, + }, + } + + if err := server.Start(); err != nil { + fmt.Printf("Error starting fabcar chaincode: %s", err.Error()) + } +} diff --git a/chaincode/fabcar/external/go.mod b/chaincode/fabcar/external/go.mod new file mode 100644 index 0000000..f76f401 --- /dev/null +++ b/chaincode/fabcar/external/go.mod @@ -0,0 +1,8 @@ +module github.com/hyperledger/fabric-samples/chaincode/fabcar/external + +go 1.13 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212 + github.com/hyperledger/fabric-contract-api-go v1.1.0 +) diff --git a/chaincode/fabcar/external/go.sum b/chaincode/fabcar/external/go.sum new file mode 100644 index 0000000..a159a45 --- /dev/null +++ b/chaincode/fabcar/external/go.sum @@ -0,0 +1,146 @@ +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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/chaincode/fabcar/external/metadata.json b/chaincode/fabcar/external/metadata.json new file mode 100644 index 0000000..43bcdc0 --- /dev/null +++ b/chaincode/fabcar/external/metadata.json @@ -0,0 +1,4 @@ +{ + "type": "external", + "label": "fabcar" +} diff --git a/chaincode/fabcar/go/fabcar.go b/chaincode/fabcar/go/fabcar.go new file mode 100644 index 0000000..935b0cd --- /dev/null +++ b/chaincode/fabcar/go/fabcar.go @@ -0,0 +1,396 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) +// utils "github.com/cd1/utils-golang" + +// SmartContract provides functions for managing a car +type SmartContract struct { + contractapi.Contract +} + +// Car describes basic details of what makes up a car +type Car struct { + Make string `json:"make"` + Model string `json:"model"` + Colour string `json:"colour"` + Owner string `json:"owner"` +} + +// ========================================================================================= +// SSE WEB MODULE DEPLOY STARTS HERE: SCHEMA +// ========================================================================================= + +type User struct { + Id string `json:"id"` + Doctype string `json:"doctype"` + Name string `json:"name"` + Email string `json:"email"` +} + +type File struct { + Id string `json:"id"` + Doctype string `json:"doctype"` + FilePath string `json:"filePath"` + Keyword string `json:"keyword"` + NumberOfKeyword int `json:"numberOfKeyword"` +} + +//here doctype will be "sentRequest" +type RequestFile struct { + Id string `json:"id"` + Doctype string `json:"doctype"` + FileId string `json:"fileid"` + OwnerId string `json:"ownerid"` + RequesterId string `json:"requesterid"` + ResponseType string `json:"responseType"` +} + +type QueryResultFile struct { + Key string `json:"Key"` + Record *File +} + +// ========================================================================================= +// SSE WEB MODULE DEPLOY ENDS HERE: SCHEMA +// ========================================================================================= + +// QueryResult structure used for handling result of query +type QueryResult struct { + Key string `json:"Key"` + Record *Car +} + +// InitLedger adds a base set of cars to the ledger +func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { + cars := []Car{ + Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"}, + Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"}, + Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"}, + Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"}, + Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"}, + Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"}, + Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"}, + Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"}, + Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"}, + Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"}, + } + + for i, car := range cars { + carAsBytes, _ := json.Marshal(car) + err := ctx.GetStub().PutState("CAR"+strconv.Itoa(i), carAsBytes) + + if err != nil { + return fmt.Errorf("Failed to put to world state. %s", err.Error()) + } + } + + return nil +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// ========================================================================================= +// SSE WEB MODULE DEPLOY STARTS HERE: FUNCTION +// ========================================================================================= + +func (s *SmartContract) Register(ctx contractapi.TransactionContextInterface, id string, email string, name string) error { + //id := utils.RandomString() + + user := User{ + Id: id, + Doctype: "User", + Name: name, + Email: email, + } + + fmt.Println("User created in register function:", user) + + userAsBytes, _ := json.Marshal(user) + + fmt.Println("User created in register function:", userAsBytes) + return ctx.GetStub().PutState(id, userAsBytes) +} + +func (s *SmartContract) QueryUserById(ctx contractapi.TransactionContextInterface, userId string) (*User, error) { + userAsBytes, err := ctx.GetStub().GetState(userId) + + if err != nil { + return nil, fmt.Errorf("Failed to read from world state. %s", err.Error()) + } + + if userAsBytes == nil { + return nil, fmt.Errorf("%s does not exist", userId) + } + + user := new(User) + _ = json.Unmarshal(userAsBytes, user) + + return user, nil +} + +func (s *SmartContract) FileUpload(ctx contractapi.TransactionContextInterface, id string, filePath, string, keyword string, numberOfKeyword int) error { + //id := utils.RandomString() + + file := File{ + Id: id, + Doctype: "File", + FilePath: filePath, + Keyword: keyword, + NumberOfKeyword: numberOfKeyword, + } + + fmt.Println("File has been uploaded", file) + + fileAsBytes, _ := json.Marshal(file) + + fmt.Println("File uploaded successfully", fileAsBytes) + return ctx.GetStub().PutState(id, fileAsBytes) +} + +func (s *SmartContract) QueryFileById(ctx contractapi.TransactionContextInterface, fileId string) (*File, error) { + fileAsBytes, err := ctx.GetStub().GetState(fileId) + + if err != nil { + return nil, fmt.Errorf("Failed to read from world state. %s", err.Error()) + } + + if fileAsBytes == nil { + return nil, fmt.Errorf("%s does not exist", fileId) + } + + file := new(File) + _ = json.Unmarshal(fileAsBytes, file) + + return file, nil +} + + + +func (s *SmartContract) FileRequestNotification(ctx contractapi.TransactionContextInterface, fileId string, ownerId string, requesterId string, responseType string) error { + id := fileId + ownerId + requesterId + //ownerid, _ := QueryUserByName(ctx, ownerName) //if shows error try s.QueryUserByName(ctx, ownerName) [also for follwing two lines] + //requesterid, _ := QueryUserByName(ctx, requesterName) + //fileid, _ := QueryFileByName(ctx, fileName) + + requestfile := RequestFile{ + Id: id, + Doctype: "Request", + FileId: fileId, + OwnerId: ownerId, + RequesterId: requesterId, + ResponseType: responseType, + } + + fmt.Println("Transaction regarding file requsest occur", requestfile) + + requestfileAsBytes, _ := json.Marshal(requestfile) + + fmt.Println("Transaction created for file request sent", requestfileAsBytes) + return ctx.GetStub().PutState(id, requestfileAsBytes) +} + +func (s *SmartContract) FileRequestAccept(ctx contractapi.TransactionContextInterface, fileId string, ownerId string, requesterId string, responseType string) error { + //id := utils.RandomString() + id := fileId + ownerId + requesterId + + requestfile := RequestFile{ + Id: id, + Doctype: "acceptRequest", + FileId: fileId, + OwnerId: ownerId, + RequesterId: requesterId, + ResponseType: responseType, + } + + fmt.Println("Transaction regarding file requsest occur", requestfile) + + requestfileAsBytes, _ := json.Marshal(requestfile) + + fmt.Println("Transaction created for file request sent", requestfileAsBytes) + return ctx.GetStub().PutState(id, requestfileAsBytes) + +} + +// ========================================================================================= +// SSE WEB MODULE DEPLOY ENDS HERE: FUNCTION +// ========================================================================================= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// CreateCar adds a new car to the world state with given details +func (s *SmartContract) CreateCar(ctx contractapi.TransactionContextInterface, carNumber string, make string, model string, colour string, owner string) error { + car := Car{ + Make: make, + Model: model, + Colour: colour, + Owner: owner, + } + + carAsBytes, _ := json.Marshal(car) + + return ctx.GetStub().PutState(carNumber, carAsBytes) +} + +// QueryCar returns the car stored in the world state with given id +func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) { + carAsBytes, err := ctx.GetStub().GetState(carNumber) + + if err != nil { + return nil, fmt.Errorf("Failed to read from world state. %s", err.Error()) + } + + if carAsBytes == nil { + return nil, fmt.Errorf("%s does not exist", carNumber) + } + + car := new(Car) + _ = json.Unmarshal(carAsBytes, car) + + return car, nil +} + +// QueryAllCars returns all cars found in world state +func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) { + startKey := "" + endKey := "" + + resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey) + + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []QueryResult{} + + for resultsIterator.HasNext() { + queryResponse, err := resultsIterator.Next() + + if err != nil { + return nil, err + } + + car := new(Car) + _ = json.Unmarshal(queryResponse.Value, car) + + queryResult := QueryResult{Key: queryResponse.Key, Record: car} + results = append(results, queryResult) + } + + return results, nil +} + +// ChangeCarOwner updates the owner field of car with given id in world state +func (s *SmartContract) ChangeCarOwner(ctx contractapi.TransactionContextInterface, carNumber string, newOwner string) error { + car, err := s.QueryCar(ctx, carNumber) + + if err != nil { + return err + } + + car.Owner = newOwner + + carAsBytes, _ := json.Marshal(car) + + return ctx.GetStub().PutState(carNumber, carAsBytes) +} + +func main() { + + chaincode, err := contractapi.NewChaincode(new(SmartContract)) + + if err != nil { + fmt.Printf("Error create fabcar chaincode: %s", err.Error()) + return + } + + if err := chaincode.Start(); err != nil { + fmt.Printf("Error starting fabcar chaincode: %s", err.Error()) + } +} diff --git a/chaincode/fabcar/go/go.mod b/chaincode/fabcar/go/go.mod new file mode 100644 index 0000000..6ae88ed --- /dev/null +++ b/chaincode/fabcar/go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/chaincode/fabcar/go + +go 1.13 + +require github.com/hyperledger/fabric-contract-api-go v1.1.0 diff --git a/chaincode/fabcar/go/go.sum b/chaincode/fabcar/go/go.sum new file mode 100644 index 0000000..a159a45 --- /dev/null +++ b/chaincode/fabcar/go/go.sum @@ -0,0 +1,146 @@ +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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/chaincode/fabcar/java/.gitignore b/chaincode/fabcar/java/.gitignore new file mode 100644 index 0000000..7005557 --- /dev/null +++ b/chaincode/fabcar/java/.gitignore @@ -0,0 +1,61 @@ + +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Gradle +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +# Eclipse files +.project +.classpath +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.externalToolBuilders/ +*.launch diff --git a/chaincode/fabcar/java/README.md b/chaincode/fabcar/java/README.md new file mode 100644 index 0000000..581c0a4 --- /dev/null +++ b/chaincode/fabcar/java/README.md @@ -0,0 +1,14 @@ +# Java FabCar contract sample + +The directions for using this sample are documented in the Hyperledger Fabric +[Writing Your First Application](https://hyperledger-fabric.readthedocs.io/en/latest/write_first_app.html) tutorial. + +The tutorial is based on JavaScript, however the same concepts are applicable when using Java. + +To install and instantiate the Java version of `FabCar`, use the following command instead of the command shown in the [Launch the network](https://hyperledger-fabric.readthedocs.io/en/release-1.4/write_first_app.html#launch-the-network) section of the tutorial: + +``` +./startFabric.sh javascript +``` + +*NOTE:* After navigating to the documentation, choose the documentation version that matches your version of Fabric diff --git a/chaincode/fabcar/java/build.gradle b/chaincode/fabcar/java/build.gradle new file mode 100644 index 0000000..32a6525 --- /dev/null +++ b/chaincode/fabcar/java/build.gradle @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'application' + id 'checkstyle' + id 'jacoco' +} + +group 'org.hyperledger.fabric.samples' +version '1.0-SNAPSHOT' + +dependencies { + compileOnly 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + implementation 'com.owlike:genson:1.5' + testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +repositories { + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + jcenter() + maven { + url 'https://jitpack.io' + } +} + +application { + mainClass = 'org.hyperledger.fabric.contract.ContractRouter' +} + +checkstyle { + toolVersion '8.21' + configFile file("config/checkstyle/checkstyle.xml") +} + +checkstyleMain { + source ='src/main/java' +} + +checkstyleTest { + source ='src/test/java' +} + +jacocoTestReport { + dependsOn test +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 1.0 + } + } + } + + finalizedBy jacocoTestReport +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +check.dependsOn jacocoTestCoverageVerification +installDist.dependsOn check diff --git a/chaincode/fabcar/java/config/checkstyle/checkstyle.xml b/chaincode/fabcar/java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..29fbde9 --- /dev/null +++ b/chaincode/fabcar/java/config/checkstyle/checkstyle.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chaincode/fabcar/java/config/checkstyle/suppressions.xml b/chaincode/fabcar/java/config/checkstyle/suppressions.xml new file mode 100644 index 0000000..8c44b0a --- /dev/null +++ b/chaincode/fabcar/java/config/checkstyle/suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/chaincode/fabcar/java/gradle/wrapper/gradle-wrapper.jar b/chaincode/fabcar/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/chaincode/fabcar/java/gradlew.bat b/chaincode/fabcar/java/gradlew.bat new file mode 100644 index 0000000..24467a1 --- /dev/null +++ b/chaincode/fabcar/java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/chaincode/fabcar/java/settings.gradle b/chaincode/fabcar/java/settings.gradle new file mode 100644 index 0000000..8ee6422 --- /dev/null +++ b/chaincode/fabcar/java/settings.gradle @@ -0,0 +1,5 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'fabcar' diff --git a/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/Car.java b/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/Car.java new file mode 100644 index 0000000..a67204a --- /dev/null +++ b/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/Car.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.fabcar; + +import java.util.Objects; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +import com.owlike.genson.annotation.JsonProperty; + +@DataType() +public final class Car { + + @Property() + private final String make; + + @Property() + private final String model; + + @Property() + private final String color; + + @Property() + private final String owner; + + public String getMake() { + return make; + } + + public String getModel() { + return model; + } + + public String getColor() { + return color; + } + + public String getOwner() { + return owner; + } + + public Car(@JsonProperty("make") final String make, @JsonProperty("model") final String model, + @JsonProperty("color") final String color, @JsonProperty("owner") final String owner) { + this.make = make; + this.model = model; + this.color = color; + this.owner = owner; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + Car other = (Car) obj; + + return Objects.deepEquals(new String[] {getMake(), getModel(), getColor(), getOwner()}, + new String[] {other.getMake(), other.getModel(), other.getColor(), other.getOwner()}); + } + + @Override + public int hashCode() { + return Objects.hash(getMake(), getModel(), getColor(), getOwner()); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + " [make=" + make + ", model=" + + model + ", color=" + color + ", owner=" + owner + "]"; + } +} diff --git a/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/CarQueryResult.java b/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/CarQueryResult.java new file mode 100644 index 0000000..934115a --- /dev/null +++ b/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/CarQueryResult.java @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.fabcar; + +import java.util.Objects; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +import com.owlike.genson.annotation.JsonProperty; + +/** + * CarQueryResult structure used for handling result of query + * + */ +@DataType() +public final class CarQueryResult { + @Property() + private final String key; + + @Property() + private final Car record; + + public CarQueryResult(@JsonProperty("Key") final String key, @JsonProperty("Record") final Car record) { + this.key = key; + this.record = record; + } + + public String getKey() { + return key; + } + + public Car getRecord() { + return record; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + CarQueryResult other = (CarQueryResult) obj; + + Boolean recordsAreEquals = this.getRecord().equals(other.getRecord()); + Boolean keysAreEquals = this.getKey().equals(other.getKey()); + + return recordsAreEquals && keysAreEquals; + } + + @Override + public int hashCode() { + return Objects.hash(this.getKey(), this.getRecord()); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + " [key=" + key + ", record=" + + record + "]"; + } +} diff --git a/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/FabCar.java b/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/FabCar.java new file mode 100644 index 0000000..89975c8 --- /dev/null +++ b/chaincode/fabcar/java/src/main/java/org/hyperledger/fabric/samples/fabcar/FabCar.java @@ -0,0 +1,190 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.fabcar; + +import java.util.ArrayList; +import java.util.List; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contact; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; + +import com.owlike.genson.Genson; + +/** + * Java implementation of the Fabric Car Contract described in the Writing Your + * First Application tutorial + */ +@Contract( + name = "FabCar", + info = @Info( + title = "FabCar contract", + description = "The hyperlegendary car contract", + version = "0.0.1-SNAPSHOT", + license = @License( + name = "Apache 2.0 License", + url = "http://www.apache.org/licenses/LICENSE-2.0.html"), + contact = @Contact( + email = "f.carr@example.com", + name = "F Carr", + url = "https://hyperledger.example.com"))) +@Default +public final class FabCar implements ContractInterface { + + private final Genson genson = new Genson(); + + private enum FabCarErrors { + CAR_NOT_FOUND, + CAR_ALREADY_EXISTS + } + + /** + * Retrieves a car with the specified key from the ledger. + * + * @param ctx the transaction context + * @param key the key + * @return the Car found on the ledger if there was one + */ + @Transaction() + public Car queryCar(final Context ctx, final String key) { + ChaincodeStub stub = ctx.getStub(); + String carState = stub.getStringState(key); + + if (carState.isEmpty()) { + String errorMessage = String.format("Car %s does not exist", key); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, FabCarErrors.CAR_NOT_FOUND.toString()); + } + + Car car = genson.deserialize(carState, Car.class); + + return car; + } + + /** + * Creates some initial Cars on the ledger. + * + * @param ctx the transaction context + */ + @Transaction() + public void initLedger(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + + String[] carData = { + "{ \"make\": \"Toyota\", \"model\": \"Prius\", \"color\": \"blue\", \"owner\": \"Tomoko\" }", + "{ \"make\": \"Ford\", \"model\": \"Mustang\", \"color\": \"red\", \"owner\": \"Brad\" }", + "{ \"make\": \"Hyundai\", \"model\": \"Tucson\", \"color\": \"green\", \"owner\": \"Jin Soo\" }", + "{ \"make\": \"Volkswagen\", \"model\": \"Passat\", \"color\": \"yellow\", \"owner\": \"Max\" }", + "{ \"make\": \"Tesla\", \"model\": \"S\", \"color\": \"black\", \"owner\": \"Adrian\" }", + "{ \"make\": \"Peugeot\", \"model\": \"205\", \"color\": \"purple\", \"owner\": \"Michel\" }", + "{ \"make\": \"Chery\", \"model\": \"S22L\", \"color\": \"white\", \"owner\": \"Aarav\" }", + "{ \"make\": \"Fiat\", \"model\": \"Punto\", \"color\": \"violet\", \"owner\": \"Pari\" }", + "{ \"make\": \"Tata\", \"model\": \"nano\", \"color\": \"indigo\", \"owner\": \"Valeria\" }", + "{ \"make\": \"Holden\", \"model\": \"Barina\", \"color\": \"brown\", \"owner\": \"Shotaro\" }" + }; + + for (int i = 0; i < carData.length; i++) { + String key = String.format("CAR%d", i); + + Car car = genson.deserialize(carData[i], Car.class); + String carState = genson.serialize(car); + stub.putStringState(key, carState); + } + } + + /** + * Creates a new car on the ledger. + * + * @param ctx the transaction context + * @param key the key for the new car + * @param make the make of the new car + * @param model the model of the new car + * @param color the color of the new car + * @param owner the owner of the new car + * @return the created Car + */ + @Transaction() + public Car createCar(final Context ctx, final String key, final String make, final String model, + final String color, final String owner) { + ChaincodeStub stub = ctx.getStub(); + + String carState = stub.getStringState(key); + if (!carState.isEmpty()) { + String errorMessage = String.format("Car %s already exists", key); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, FabCarErrors.CAR_ALREADY_EXISTS.toString()); + } + + Car car = new Car(make, model, color, owner); + carState = genson.serialize(car); + stub.putStringState(key, carState); + + return car; + } + + /** + * Retrieves all cars from the ledger. + * + * @param ctx the transaction context + * @return array of Cars found on the ledger + */ + @Transaction() + public String queryAllCars(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + + final String startKey = "CAR1"; + final String endKey = "CAR99"; + List queryResults = new ArrayList(); + + QueryResultsIterator results = stub.getStateByRange(startKey, endKey); + + for (KeyValue result: results) { + Car car = genson.deserialize(result.getStringValue(), Car.class); + queryResults.add(new CarQueryResult(result.getKey(), car)); + } + + final String response = genson.serialize(queryResults); + + return response; + } + + /** + * Changes the owner of a car on the ledger. + * + * @param ctx the transaction context + * @param key the key + * @param newOwner the new owner + * @return the updated Car + */ + @Transaction() + public Car changeCarOwner(final Context ctx, final String key, final String newOwner) { + ChaincodeStub stub = ctx.getStub(); + + String carState = stub.getStringState(key); + + if (carState.isEmpty()) { + String errorMessage = String.format("Car %s does not exist", key); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, FabCarErrors.CAR_NOT_FOUND.toString()); + } + + Car car = genson.deserialize(carState, Car.class); + + Car newCar = new Car(car.getMake(), car.getModel(), car.getColor(), newOwner); + String newCarState = genson.serialize(newCar); + stub.putStringState(key, newCarState); + + return newCar; + } +} diff --git a/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarQueryResultTest.java b/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarQueryResultTest.java new file mode 100644 index 0000000..1bcacd6 --- /dev/null +++ b/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarQueryResultTest.java @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.fabcar; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public final class CarQueryResultTest { + + @Nested + class Equality { + + @Test + public void isReflexive() { + CarQueryResult cqr = new CarQueryResult("CAR1", new Car("Toyota", "Prius", "blue", "Tomoko")); + + assertThat(cqr).isEqualTo(cqr); + } + + @Test + public void isSymmetric() { + Car car = new Car("Toyota", "Prius", "blue", "Tomoko"); + CarQueryResult cqrA = new CarQueryResult("CAR1", car); + CarQueryResult cqrB = new CarQueryResult("CAR1", car); + + assertThat(cqrA).isEqualTo(cqrB); + assertThat(cqrB).isEqualTo(cqrA); + } + + @Test + public void isTransitive() { + Car car = new Car("Toyota", "Prius", "blue", "Tomoko"); + CarQueryResult cqrA = new CarQueryResult("CAR1", car); + CarQueryResult cqrB = new CarQueryResult("CAR1", car); + CarQueryResult cqrC = new CarQueryResult("CAR1", car); + + assertThat(cqrA).isEqualTo(cqrB); + assertThat(cqrB).isEqualTo(cqrC); + assertThat(cqrA).isEqualTo(cqrC); + } + + @Test + public void handlesKeyInequality() { + CarQueryResult cqrA = new CarQueryResult("CAR1", new Car("Toyota", "Prius", "blue", "Tomoko")); + CarQueryResult cqrB = new CarQueryResult("CAR2", new Car("Toyota", "Prius", "blue", "Tomoko")); + + assertThat(cqrA).isNotEqualTo(cqrB); + } + + @Test + public void handlesRecordInequality() { + CarQueryResult cqrA = new CarQueryResult("CAR1", new Car("Toyota", "Prius", "blue", "Tomoko")); + CarQueryResult cqrB = new CarQueryResult("CAR1", new Car("Ford", "Mustang", "red", "Brad")); + + assertThat(cqrA).isNotEqualTo(cqrB); + } + + @Test + public void handlesKeyRecordInequality() { + CarQueryResult cqrA = new CarQueryResult("CAR1", new Car("Toyota", "Prius", "blue", "Tomoko")); + CarQueryResult cqrB = new CarQueryResult("CAR2", new Car("Ford", "Mustang", "red", "Brad")); + + assertThat(cqrA).isNotEqualTo(cqrB); + } + + @Test + public void handlesOtherObjects() { + CarQueryResult cqrA = new CarQueryResult("CAR1", new Car("Toyota", "Prius", "blue", "Tomoko")); + String cqrB = "not a car"; + + assertThat(cqrA).isNotEqualTo(cqrB); + } + + @Test + public void handlesNull() { + CarQueryResult cqr = new CarQueryResult("CAR1", new Car("Toyota", "Prius", "blue", "Tomoko")); + + assertThat(cqr).isNotEqualTo(null); + } + } + + @Test + public void toStringIdentifiesCarQueryResult() { + CarQueryResult cqr = new CarQueryResult("CAR1", new Car("Toyota", "Prius", "blue", "Tomoko")); + + assertThat(cqr.toString()).isEqualTo("CarQueryResult@65766eb3 [key=CAR1, " + + "record=Car@61a77e4f [make=Toyota, model=Prius, color=blue, owner=Tomoko]]"); + } +} diff --git a/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarTest.java b/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarTest.java new file mode 100644 index 0000000..5c7b4fc --- /dev/null +++ b/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/CarTest.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.fabcar; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public final class CarTest { + + @Nested + class Equality { + + @Test + public void isReflexive() { + Car car = new Car("Toyota", "Prius", "blue", "Tomoko"); + + assertThat(car).isEqualTo(car); + } + + @Test + public void isSymmetric() { + Car carA = new Car("Toyota", "Prius", "blue", "Tomoko"); + Car carB = new Car("Toyota", "Prius", "blue", "Tomoko"); + + assertThat(carA).isEqualTo(carB); + assertThat(carB).isEqualTo(carA); + } + + @Test + public void isTransitive() { + Car carA = new Car("Toyota", "Prius", "blue", "Tomoko"); + Car carB = new Car("Toyota", "Prius", "blue", "Tomoko"); + Car carC = new Car("Toyota", "Prius", "blue", "Tomoko"); + + assertThat(carA).isEqualTo(carB); + assertThat(carB).isEqualTo(carC); + assertThat(carA).isEqualTo(carC); + } + + @Test + public void handlesInequality() { + Car carA = new Car("Toyota", "Prius", "blue", "Tomoko"); + Car carB = new Car("Ford", "Mustang", "red", "Brad"); + + assertThat(carA).isNotEqualTo(carB); + } + + @Test + public void handlesOtherObjects() { + Car carA = new Car("Toyota", "Prius", "blue", "Tomoko"); + String carB = "not a car"; + + assertThat(carA).isNotEqualTo(carB); + } + + @Test + public void handlesNull() { + Car car = new Car("Toyota", "Prius", "blue", "Tomoko"); + + assertThat(car).isNotEqualTo(null); + } + } + + @Test + public void toStringIdentifiesCar() { + Car car = new Car("Toyota", "Prius", "blue", "Tomoko"); + + assertThat(car.toString()).isEqualTo("Car@61a77e4f [make=Toyota, model=Prius, color=blue, owner=Tomoko]"); + } +} diff --git a/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/FabCarTest.java b/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/FabCarTest.java new file mode 100644 index 0000000..ea27f47 --- /dev/null +++ b/chaincode/fabcar/java/src/test/java/org/hyperledger/fabric/samples/fabcar/FabCarTest.java @@ -0,0 +1,264 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.fabcar; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +public final class FabCarTest { + + private final class MockKeyValue implements KeyValue { + + private final String key; + private final String value; + + MockKeyValue(final String key, final String value) { + super(); + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public String getStringValue() { + return this.value; + } + + @Override + public byte[] getValue() { + return this.value.getBytes(); + } + + } + + private final class MockCarResultsIterator implements QueryResultsIterator { + + private final List carList; + + MockCarResultsIterator() { + super(); + + carList = new ArrayList(); + + carList.add(new MockKeyValue("CAR0", + "{\"color\":\"blue\",\"make\":\"Toyota\",\"model\":\"Prius\",\"owner\":\"Tomoko\"}")); + carList.add(new MockKeyValue("CAR1", + "{\"color\":\"red\",\"make\":\"Ford\",\"model\":\"Mustang\",\"owner\":\"Brad\"}")); + carList.add(new MockKeyValue("CAR2", + "{\"color\":\"green\",\"make\":\"Hyundai\",\"model\":\"Tucson\",\"owner\":\"Jin Soo\"}")); + carList.add(new MockKeyValue("CAR7", + "{\"color\":\"violet\",\"make\":\"Fiat\",\"model\":\"Punto\",\"owner\":\"Pari\"}")); + carList.add(new MockKeyValue("CAR9", + "{\"color\":\"brown\",\"make\":\"Holden\",\"model\":\"Barina\",\"owner\":\"Shotaro\"}")); + } + + @Override + public Iterator iterator() { + return carList.iterator(); + } + + @Override + public void close() throws Exception { + // do nothing + } + + } + + @Test + public void invokeUnknownTransaction() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + + Throwable thrown = catchThrowable(() -> { + contract.unknownTransaction(ctx); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Undefined contract method called"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo(null); + + verifyZeroInteractions(ctx); + } + + @Nested + class InvokeQueryCarTransaction { + + @Test + public void whenCarExists() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("CAR0")) + .thenReturn("{\"color\":\"blue\",\"make\":\"Toyota\",\"model\":\"Prius\",\"owner\":\"Tomoko\"}"); + + Car car = contract.queryCar(ctx, "CAR0"); + + assertThat(car).isEqualTo(new Car("Toyota", "Prius", "blue", "Tomoko")); + } + + @Test + public void whenCarDoesNotExist() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("CAR0")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.queryCar(ctx, "CAR0"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Car CAR0 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("CAR_NOT_FOUND".getBytes()); + } + } + + @Test + void invokeInitLedgerTransaction() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + + contract.initLedger(ctx); + + InOrder inOrder = inOrder(stub); + inOrder.verify(stub).putStringState("CAR0", + "{\"color\":\"blue\",\"make\":\"Toyota\",\"model\":\"Prius\",\"owner\":\"Tomoko\"}"); + inOrder.verify(stub).putStringState("CAR1", + "{\"color\":\"red\",\"make\":\"Ford\",\"model\":\"Mustang\",\"owner\":\"Brad\"}"); + inOrder.verify(stub).putStringState("CAR2", + "{\"color\":\"green\",\"make\":\"Hyundai\",\"model\":\"Tucson\",\"owner\":\"Jin Soo\"}"); + inOrder.verify(stub).putStringState("CAR3", + "{\"color\":\"yellow\",\"make\":\"Volkswagen\",\"model\":\"Passat\",\"owner\":\"Max\"}"); + inOrder.verify(stub).putStringState("CAR4", + "{\"color\":\"black\",\"make\":\"Tesla\",\"model\":\"S\",\"owner\":\"Adrian\"}"); + inOrder.verify(stub).putStringState("CAR5", + "{\"color\":\"purple\",\"make\":\"Peugeot\",\"model\":\"205\",\"owner\":\"Michel\"}"); + inOrder.verify(stub).putStringState("CAR6", + "{\"color\":\"white\",\"make\":\"Chery\",\"model\":\"S22L\",\"owner\":\"Aarav\"}"); + inOrder.verify(stub).putStringState("CAR7", + "{\"color\":\"violet\",\"make\":\"Fiat\",\"model\":\"Punto\",\"owner\":\"Pari\"}"); + inOrder.verify(stub).putStringState("CAR8", + "{\"color\":\"indigo\",\"make\":\"Tata\",\"model\":\"nano\",\"owner\":\"Valeria\"}"); + inOrder.verify(stub).putStringState("CAR9", + "{\"color\":\"brown\",\"make\":\"Holden\",\"model\":\"Barina\",\"owner\":\"Shotaro\"}"); + } + + @Nested + class InvokeCreateCarTransaction { + + @Test + public void whenCarExists() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("CAR0")) + .thenReturn("{\"color\":\"blue\",\"make\":\"Toyota\",\"model\":\"Prius\",\"owner\":\"Tomoko\"}"); + + Throwable thrown = catchThrowable(() -> { + contract.createCar(ctx, "CAR0", "Nissan", "Leaf", "green", "Siobhán"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Car CAR0 already exists"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("CAR_ALREADY_EXISTS".getBytes()); + } + + @Test + public void whenCarDoesNotExist() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("CAR0")).thenReturn(""); + + Car car = contract.createCar(ctx, "CAR0", "Nissan", "Leaf", "green", "Siobhán"); + + assertThat(car).isEqualTo(new Car("Nissan", "Leaf", "green", "Siobhán")); + } + } + + @Test + void invokeQueryAllCarsTransaction() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStateByRange("CAR1", "CAR99")).thenReturn(new MockCarResultsIterator()); + + String cars = contract.queryAllCars(ctx); + + assertThat(cars).isEqualTo("[{\"key\":\"CAR0\"," + + "\"record\":{\"color\":\"blue\",\"make\":\"Toyota\",\"model\":\"Prius\",\"owner\":\"Tomoko\"}}," + + "{\"key\":\"CAR1\"," + + "\"record\":{\"color\":\"red\",\"make\":\"Ford\",\"model\":\"Mustang\",\"owner\":\"Brad\"}}," + + "{\"key\":\"CAR2\"," + + "\"record\":{\"color\":\"green\",\"make\":\"Hyundai\",\"model\":\"Tucson\",\"owner\":\"Jin Soo\"}}," + + "{\"key\":\"CAR7\"," + + "\"record\":{\"color\":\"violet\",\"make\":\"Fiat\",\"model\":\"Punto\",\"owner\":\"Pari\"}}," + + "{\"key\":\"CAR9\"," + + "\"record\":{\"color\":\"brown\",\"make\":\"Holden\",\"model\":\"Barina\",\"owner\":\"Shotaro\"}}]"); + } + + @Nested + class ChangeCarOwnerTransaction { + + @Test + public void whenCarExists() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("CAR0")) + .thenReturn("{\"color\":\"blue\",\"make\":\"Toyota\",\"model\":\"Prius\",\"owner\":\"Tomoko\"}"); + + Car car = contract.changeCarOwner(ctx, "CAR0", "Dr Evil"); + + assertThat(car).isEqualTo(new Car("Toyota", "Prius", "blue", "Dr Evil")); + } + + @Test + public void whenCarDoesNotExist() { + FabCar contract = new FabCar(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("CAR0")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.changeCarOwner(ctx, "CAR0", "Dr Evil"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Car CAR0 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("CAR_NOT_FOUND".getBytes()); + } + } +} diff --git a/chaincode/fabcar/javascript/.editorconfig b/chaincode/fabcar/javascript/.editorconfig new file mode 100755 index 0000000..75a13be --- /dev/null +++ b/chaincode/fabcar/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/chaincode/fabcar/javascript/.eslintignore b/chaincode/fabcar/javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/chaincode/fabcar/javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/chaincode/fabcar/javascript/.eslintrc.js b/chaincode/fabcar/javascript/.eslintrc.js new file mode 100644 index 0000000..6d5751a --- /dev/null +++ b/chaincode/fabcar/javascript/.eslintrc.js @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + env: { + node: 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/chaincode/fabcar/javascript/.gitignore b/chaincode/fabcar/javascript/.gitignore new file mode 100644 index 0000000..a00ca94 --- /dev/null +++ b/chaincode/fabcar/javascript/.gitignore @@ -0,0 +1,77 @@ +# +# 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/ + +# 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/chaincode/fabcar/javascript/index.js b/chaincode/fabcar/javascript/index.js new file mode 100644 index 0000000..493f4a1 --- /dev/null +++ b/chaincode/fabcar/javascript/index.js @@ -0,0 +1,12 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const FabCar = require('./lib/fabcar'); + +module.exports.FabCar = FabCar; +module.exports.contracts = [ FabCar ]; diff --git a/chaincode/fabcar/javascript/lib/fabcar.js b/chaincode/fabcar/javascript/lib/fabcar.js new file mode 100644 index 0000000..1220e5f --- /dev/null +++ b/chaincode/fabcar/javascript/lib/fabcar.js @@ -0,0 +1,145 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Contract } = require('fabric-contract-api'); + +class FabCar extends Contract { + + async initLedger(ctx) { + console.info('============= START : Initialize Ledger ==========='); + const cars = [ + { + color: 'blue', + make: 'Toyota', + model: 'Prius', + owner: 'Tomoko', + }, + { + color: 'red', + make: 'Ford', + model: 'Mustang', + owner: 'Brad', + }, + { + color: 'green', + make: 'Hyundai', + model: 'Tucson', + owner: 'Jin Soo', + }, + { + color: 'yellow', + make: 'Volkswagen', + model: 'Passat', + owner: 'Max', + }, + { + color: 'black', + make: 'Tesla', + model: 'S', + owner: 'Adriana', + }, + { + color: 'purple', + make: 'Peugeot', + model: '205', + owner: 'Michel', + }, + { + color: 'white', + make: 'Chery', + model: 'S22L', + owner: 'Aarav', + }, + { + color: 'violet', + make: 'Fiat', + model: 'Punto', + owner: 'Pari', + }, + { + color: 'indigo', + make: 'Tata', + model: 'Nano', + owner: 'Valeria', + }, + { + color: 'brown', + make: 'Holden', + model: 'Barina', + owner: 'Shotaro', + }, + ]; + + for (let i = 0; i < cars.length; i++) { + cars[i].docType = 'car'; + await ctx.stub.putState('CAR' + i, Buffer.from(JSON.stringify(cars[i]))); + console.info('Added <--> ', cars[i]); + } + console.info('============= END : Initialize Ledger ==========='); + } + + async queryCar(ctx, carNumber) { + const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state + if (!carAsBytes || carAsBytes.length === 0) { + throw new Error(`${carNumber} does not exist`); + } + console.log(carAsBytes.toString()); + return carAsBytes.toString(); + } + + async createCar(ctx, carNumber, make, model, color, owner) { + console.info('============= START : Create Car ==========='); + + const car = { + color, + docType: 'car', + make, + model, + owner, + }; + + await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); + console.info('============= END : Create Car ==========='); + } + + async queryAllCars(ctx) { + const startKey = ''; + const endKey = ''; + const allResults = []; + for await (const {key, value} of ctx.stub.getStateByRange(startKey, endKey)) { + const strValue = Buffer.from(value).toString('utf8'); + let record; + try { + record = JSON.parse(strValue); + } catch (err) { + console.log(err); + record = strValue; + } + allResults.push({ Key: key, Record: record }); + } + console.info(allResults); + return JSON.stringify(allResults); + } + + async changeCarOwner(ctx, carNumber, newOwner) { + console.info('============= START : changeCarOwner ==========='); + + const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state + if (!carAsBytes || carAsBytes.length === 0) { + throw new Error(`${carNumber} does not exist`); + } + const car = JSON.parse(carAsBytes.toString()); + car.owner = newOwner; + + await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); + console.info('============= END : changeCarOwner ==========='); + } + +} + +module.exports = FabCar; diff --git a/chaincode/fabcar/javascript/package.json b/chaincode/fabcar/javascript/package.json new file mode 100644 index 0000000..1adb28b --- /dev/null +++ b/chaincode/fabcar/javascript/package.json @@ -0,0 +1,47 @@ +{ + "name": "fabcar", + "version": "1.0.0", + "description": "FabCar contract implemented in JavaScript", + "main": "index.js", + "engines": { + "node": ">=12", + "npm": ">=6.9" + }, + "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": "^5.2.0", + "nyc": "^14.1.1", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/chaincode/fabcar/typescript/.editorconfig b/chaincode/fabcar/typescript/.editorconfig new file mode 100755 index 0000000..75a13be --- /dev/null +++ b/chaincode/fabcar/typescript/.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/chaincode/fabcar/typescript/.gitignore b/chaincode/fabcar/typescript/.gitignore new file mode 100644 index 0000000..69d6a33 --- /dev/null +++ b/chaincode/fabcar/typescript/.gitignore @@ -0,0 +1,81 @@ +# +# 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/ + +# 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 + +# Compiled TypeScript files +dist + diff --git a/chaincode/fabcar/typescript/package.json b/chaincode/fabcar/typescript/package.json new file mode 100644 index 0000000..7cd3837 --- /dev/null +++ b/chaincode/fabcar/typescript/package.json @@ -0,0 +1,62 @@ +{ + "name": "fabcar", + "version": "1.0.0", + "description": "FabCar contract implemented in TypeScript", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "pretest": "npm run lint", + "test": "nyc mocha -r ts-node/register src/**/*.spec.ts", + "start": "fabric-chaincode-node start", + "build": "tsc", + "build:watch": "tsc -w", + "prepublishOnly": "npm run build" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.5", + "@types/node": "^10.12.10", + "@types/sinon": "^5.0.7", + "@types/sinon-chai": "^3.2.1", + "chai": "^4.2.0", + "mocha": "^5.2.0", + "nyc": "^14.1.1", + "sinon": "^7.1.1", + "sinon-chai": "^3.3.0", + "ts-node": "^7.0.1", + "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/chaincode/fabcar/typescript/src/car.ts b/chaincode/fabcar/typescript/src/car.ts new file mode 100644 index 0000000..ba10162 --- /dev/null +++ b/chaincode/fabcar/typescript/src/car.ts @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +export class Car { + public docType?: string; + public color: string; + public make: string; + public model: string; + public owner: string; +} diff --git a/chaincode/fabcar/typescript/src/fabcar.ts b/chaincode/fabcar/typescript/src/fabcar.ts new file mode 100644 index 0000000..597da7e --- /dev/null +++ b/chaincode/fabcar/typescript/src/fabcar.ts @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Context, Contract } from 'fabric-contract-api'; +import { Car } from './car'; + +export class FabCar extends Contract { + + public async initLedger(ctx: Context) { + console.info('============= START : Initialize Ledger ==========='); + const cars: Car[] = [ + { + color: 'blue', + make: 'Toyota', + model: 'Prius', + owner: 'Tomoko', + }, + { + color: 'red', + make: 'Ford', + model: 'Mustang', + owner: 'Brad', + }, + { + color: 'green', + make: 'Hyundai', + model: 'Tucson', + owner: 'Jin Soo', + }, + { + color: 'yellow', + make: 'Volkswagen', + model: 'Passat', + owner: 'Max', + }, + { + color: 'black', + make: 'Tesla', + model: 'S', + owner: 'Adriana', + }, + { + color: 'purple', + make: 'Peugeot', + model: '205', + owner: 'Michel', + }, + { + color: 'white', + make: 'Chery', + model: 'S22L', + owner: 'Aarav', + }, + { + color: 'violet', + make: 'Fiat', + model: 'Punto', + owner: 'Pari', + }, + { + color: 'indigo', + make: 'Tata', + model: 'Nano', + owner: 'Valeria', + }, + { + color: 'brown', + make: 'Holden', + model: 'Barina', + owner: 'Shotaro', + }, + ]; + + for (let i = 0; i < cars.length; i++) { + cars[i].docType = 'car'; + await ctx.stub.putState('CAR' + i, Buffer.from(JSON.stringify(cars[i]))); + console.info('Added <--> ', cars[i]); + } + console.info('============= END : Initialize Ledger ==========='); + } + + public async queryCar(ctx: Context, carNumber: string): Promise { + const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state + if (!carAsBytes || carAsBytes.length === 0) { + throw new Error(`${carNumber} does not exist`); + } + console.log(carAsBytes.toString()); + return carAsBytes.toString(); + } + + public async createCar(ctx: Context, carNumber: string, make: string, model: string, color: string, owner: string) { + console.info('============= START : Create Car ==========='); + + const car: Car = { + color, + docType: 'car', + make, + model, + owner, + }; + + await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); + console.info('============= END : Create Car ==========='); + } + + public async queryAllCars(ctx: Context): Promise { + const startKey = ''; + const endKey = ''; + const allResults = []; + for await (const {key, value} of ctx.stub.getStateByRange(startKey, endKey)) { + const strValue = Buffer.from(value).toString('utf8'); + let record; + try { + record = JSON.parse(strValue); + } catch (err) { + console.log(err); + record = strValue; + } + allResults.push({ Key: key, Record: record }); + } + console.info(allResults); + return JSON.stringify(allResults); + } + + public async changeCarOwner(ctx: Context, carNumber: string, newOwner: string) { + console.info('============= START : changeCarOwner ==========='); + + const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state + if (!carAsBytes || carAsBytes.length === 0) { + throw new Error(`${carNumber} does not exist`); + } + const car: Car = JSON.parse(carAsBytes.toString()); + car.owner = newOwner; + + await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); + console.info('============= END : changeCarOwner ==========='); + } + +} diff --git a/chaincode/fabcar/typescript/src/index.ts b/chaincode/fabcar/typescript/src/index.ts new file mode 100644 index 0000000..c0a2fcf --- /dev/null +++ b/chaincode/fabcar/typescript/src/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FabCar } from './fabcar'; +export { FabCar } from './fabcar'; + +export const contracts: any[] = [ FabCar ]; diff --git a/chaincode/fabcar/typescript/tsconfig.json b/chaincode/fabcar/typescript/tsconfig.json new file mode 100644 index 0000000..8c96ea0 --- /dev/null +++ b/chaincode/fabcar/typescript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "es2017", + "moduleResolution": "node", + "module": "commonjs", + "declaration": true, + "sourceMap": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/chaincode/fabcar/typescript/tslint.json b/chaincode/fabcar/typescript/tslint.json new file mode 100644 index 0000000..33ccbf3 --- /dev/null +++ b/chaincode/fabcar/typescript/tslint.json @@ -0,0 +1,21 @@ +{ + "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"] + }, + "rulesDirectory": [] +} diff --git a/chaincode/marbles02/go/META-INF/statedb/couchdb/indexes/indexOwner.json b/chaincode/marbles02/go/META-INF/statedb/couchdb/indexes/indexOwner.json new file mode 100644 index 0000000..305f090 --- /dev/null +++ b/chaincode/marbles02/go/META-INF/statedb/couchdb/indexes/indexOwner.json @@ -0,0 +1 @@ +{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} diff --git a/chaincode/marbles02/go/go.mod b/chaincode/marbles02/go/go.mod new file mode 100644 index 0000000..d61ab3e --- /dev/null +++ b/chaincode/marbles02/go/go.mod @@ -0,0 +1,12 @@ +module github.com/hyperledger/fabric-samples/chaincode/marbles02/go + +go 1.12 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 + github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect + golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + golang.org/x/text v0.3.2 // indirect + google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect +) diff --git a/chaincode/marbles02/go/go.sum b/chaincode/marbles02/go/go.sum new file mode 100644 index 0000000..66ba37b --- /dev/null +++ b/chaincode/marbles02/go/go.sum @@ -0,0 +1,85 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +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/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +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/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 h1:VEm3tPRTCzq3J/1XpVERh1PbOSnshUVwx2G5s3cLiTw= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85/go.mod h1:HZK6PKLWrvdD/t0oSLiyaRaUM6fZ7qjJuOlb0zrn0mo= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 h1:WzttYAPO5xkQ87ZrxzEhvDZknfarSNu1PZt3NPMTE3Y= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/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-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-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +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/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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/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= diff --git a/chaincode/marbles02/go/marbles_chaincode.go b/chaincode/marbles02/go/marbles_chaincode.go new file mode 100644 index 0000000..d47fe92 --- /dev/null +++ b/chaincode/marbles02/go/marbles_chaincode.go @@ -0,0 +1,755 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ + +// ====CHAINCODE EXECUTION SAMPLES (CLI) ================== + +// ==== Invoke marbles ==== +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble1","blue","35","tom"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble2","red","50","tom"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble3","blue","70","tom"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["transferMarble","marble2","jerry"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["transferMarblesBasedOnColor","blue","jerry"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["delete","marble1"]}' + +// ==== Query marbles ==== +// peer chaincode query -C myc1 -n marbles -c '{"Args":["readMarble","marble1"]}' +// peer chaincode query -C myc1 -n marbles -c '{"Args":["getMarblesByRange","marble1","marble3"]}' +// peer chaincode query -C myc1 -n marbles -c '{"Args":["getHistoryForMarble","marble1"]}' + +// Rich Query (Only supported if CouchDB is used as state database): +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarblesByOwner","tom"]}' +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarbles","{\"selector\":{\"owner\":\"tom\"}}"]}' + +// Rich Query with Pagination (Only supported if CouchDB is used as state database): +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarblesWithPagination","{\"selector\":{\"owner\":\"tom\"}}","3",""]}' + +// INDEXES TO SUPPORT COUCHDB RICH QUERIES +// +// Indexes in CouchDB are required in order to make JSON queries efficient and are required for +// any JSON query with a sort. As of Hyperledger Fabric 1.1, indexes may be packaged alongside +// chaincode in a META-INF/statedb/couchdb/indexes directory. Each index must be defined in its own +// text file with extension *.json with the index definition formatted in JSON following the +// CouchDB index JSON syntax as documented at: +// http://docs.couchdb.org/en/2.1.1/api/database/find.html#db-index +// +// This marbles02 example chaincode demonstrates a packaged +// index which you can find in META-INF/statedb/couchdb/indexes/indexOwner.json. +// For deployment of chaincode to production environments, it is recommended +// to define any indexes alongside chaincode so that the chaincode and supporting indexes +// are deployed automatically as a unit, once the chaincode has been installed on a peer and +// instantiated on a channel. See Hyperledger Fabric documentation for more details. +// +// If you have access to the your peer's CouchDB state database in a development environment, +// you may want to iteratively test various indexes in support of your chaincode queries. You +// can use the CouchDB Fauxton interface or a command line curl utility to create and update +// indexes. Then once you finalize an index, include the index definition alongside your +// chaincode in the META-INF/statedb/couchdb/indexes directory, for packaging and deployment +// to managed environments. +// +// In the examples below you can find index definitions that support marbles02 +// chaincode queries, along with the syntax that you can use in development environments +// to create the indexes in the CouchDB Fauxton interface or a curl command line utility. +// + +//Example hostname:port configurations to access CouchDB. +// +//To access CouchDB docker container from within another docker container or from vagrant environments: +// http://couchdb:5984/ +// +//Inside couchdb docker container +// http://127.0.0.1:5984/ + +// Index for docType, owner. +// +// Example curl command line to define index in the CouchDB channel_chaincode database +// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[\"docType\",\"owner\"]},\"name\":\"indexOwner\",\"ddoc\":\"indexOwnerDoc\",\"type\":\"json\"}" http://hostname:port/myc1_marbles/_index +// + +// Index for docType, owner, size (descending order). +// +// Example curl command line to define index in the CouchDB channel_chaincode database +// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[{\"size\":\"desc\"},{\"docType\":\"desc\"},{\"owner\":\"desc\"}]},\"ddoc\":\"indexSizeSortDoc\", \"name\":\"indexSizeSortDesc\",\"type\":\"json\"}" http://hostname:port/myc1_marbles/_index + +// Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database): +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarbles","{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}' + +// Rich Query with index design doc specified only (Only supported if CouchDB is used as state database): +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarbles","{\"selector\":{\"docType\":{\"$eq\":\"marble\"},\"owner\":{\"$eq\":\"tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}' + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/hyperledger/fabric-chaincode-go/shim" + pb "github.com/hyperledger/fabric-protos-go/peer" +) + +// SimpleChaincode example simple Chaincode implementation +type SimpleChaincode struct { +} + +type marble struct { + ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database + Name string `json:"name"` //the fieldtags are needed to keep case from bouncing around + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` +} + +// =================================================================================== +// Main +// =================================================================================== +func main() { + err := shim.Start(new(SimpleChaincode)) + if err != nil { + fmt.Printf("Error starting Simple chaincode: %s", err) + } +} + +// Init initializes chaincode +// =========================== +func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response { + return shim.Success(nil) +} + +// Invoke - Our entry point for Invocations +// ======================================== +func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { + function, args := stub.GetFunctionAndParameters() + fmt.Println("invoke is running " + function) + + // Handle different functions + if function == "initMarble" { //create a new marble + return t.initMarble(stub, args) + } else if function == "transferMarble" { //change owner of a specific marble + return t.transferMarble(stub, args) + } else if function == "transferMarblesBasedOnColor" { //transfer all marbles of a certain color + return t.transferMarblesBasedOnColor(stub, args) + } else if function == "delete" { //delete a marble + return t.delete(stub, args) + } else if function == "readMarble" { //read a marble + return t.readMarble(stub, args) + } else if function == "queryMarblesByOwner" { //find marbles for owner X using rich query + return t.queryMarblesByOwner(stub, args) + } else if function == "queryMarbles" { //find marbles based on an ad hoc rich query + return t.queryMarbles(stub, args) + } else if function == "getHistoryForMarble" { //get history of values for a marble + return t.getHistoryForMarble(stub, args) + } else if function == "getMarblesByRange" { //get marbles based on range query + return t.getMarblesByRange(stub, args) + } else if function == "getMarblesByRangeWithPagination" { + return t.getMarblesByRangeWithPagination(stub, args) + } else if function == "queryMarblesWithPagination" { + return t.queryMarblesWithPagination(stub, args) + } + + fmt.Println("invoke did not find func: " + function) //error + return shim.Error("Received unknown function invocation") +} + +// ============================================================ +// initMarble - create a new marble, store into chaincode state +// ============================================================ +func (t *SimpleChaincode) initMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response { + var err error + + // 0 1 2 3 + // "asdf", "blue", "35", "bob" + if len(args) != 4 { + return shim.Error("Incorrect number of arguments. Expecting 4") + } + + // ==== Input sanitation ==== + fmt.Println("- start init marble") + if len(args[0]) <= 0 { + return shim.Error("1st argument must be a non-empty string") + } + if len(args[1]) <= 0 { + return shim.Error("2nd argument must be a non-empty string") + } + if len(args[2]) <= 0 { + return shim.Error("3rd argument must be a non-empty string") + } + if len(args[3]) <= 0 { + return shim.Error("4th argument must be a non-empty string") + } + marbleName := args[0] + color := strings.ToLower(args[1]) + owner := strings.ToLower(args[3]) + size, err := strconv.Atoi(args[2]) + if err != nil { + return shim.Error("3rd argument must be a numeric string") + } + + // ==== Check if marble already exists ==== + marbleAsBytes, err := stub.GetState(marbleName) + if err != nil { + return shim.Error("Failed to get marble: " + err.Error()) + } else if marbleAsBytes != nil { + fmt.Println("This marble already exists: " + marbleName) + return shim.Error("This marble already exists: " + marbleName) + } + + // ==== Create marble object and marshal to JSON ==== + objectType := "marble" + marble := &marble{objectType, marbleName, color, size, owner} + marbleJSONasBytes, err := json.Marshal(marble) + if err != nil { + return shim.Error(err.Error()) + } + //Alternatively, build the marble json string manually if you don't want to use struct marshalling + //marbleJSONasString := `{"docType":"Marble", "name": "` + marbleName + `", "color": "` + color + `", "size": ` + strconv.Itoa(size) + `, "owner": "` + owner + `"}` + //marbleJSONasBytes := []byte(str) + + // === Save marble to state === + err = stub.PutState(marbleName, marbleJSONasBytes) + if err != nil { + return shim.Error(err.Error()) + } + + // ==== Index the marble to enable color-based range queries, e.g. return all blue marbles ==== + // An 'index' is a normal key/value entry in state. + // The key is a composite key, with the elements that you want to range query on listed first. + // In our case, the composite key is based on indexName~color~name. + // This will enable very efficient state range queries based on composite keys matching indexName~color~* + indexName := "color~name" + colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marble.Color, marble.Name}) + if err != nil { + return shim.Error(err.Error()) + } + // Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble. + // Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value + value := []byte{0x00} + stub.PutState(colorNameIndexKey, value) + + // ==== Marble saved and indexed. Return success ==== + fmt.Println("- end init marble") + return shim.Success(nil) +} + +// =============================================== +// readMarble - read a marble from chaincode state +// =============================================== +func (t *SimpleChaincode) readMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response { + var name, jsonResp string + var err error + + if len(args) != 1 { + return shim.Error("Incorrect number of arguments. Expecting name of the marble to query") + } + + name = args[0] + valAsbytes, err := stub.GetState(name) //get the marble from chaincode state + if err != nil { + jsonResp = "{\"Error\":\"Failed to get state for " + name + "\"}" + return shim.Error(jsonResp) + } else if valAsbytes == nil { + jsonResp = "{\"Error\":\"Marble does not exist: " + name + "\"}" + return shim.Error(jsonResp) + } + + return shim.Success(valAsbytes) +} + +// ================================================== +// delete - remove a marble key/value pair from state +// ================================================== +func (t *SimpleChaincode) delete(stub shim.ChaincodeStubInterface, args []string) pb.Response { + var jsonResp string + var marbleJSON marble + if len(args) != 1 { + return shim.Error("Incorrect number of arguments. Expecting 1") + } + marbleName := args[0] + + // to maintain the color~name index, we need to read the marble first and get its color + valAsbytes, err := stub.GetState(marbleName) //get the marble from chaincode state + if err != nil { + jsonResp = "{\"Error\":\"Failed to get state for " + marbleName + "\"}" + return shim.Error(jsonResp) + } else if valAsbytes == nil { + jsonResp = "{\"Error\":\"Marble does not exist: " + marbleName + "\"}" + return shim.Error(jsonResp) + } + + err = json.Unmarshal([]byte(valAsbytes), &marbleJSON) + if err != nil { + jsonResp = "{\"Error\":\"Failed to decode JSON of: " + marbleName + "\"}" + return shim.Error(jsonResp) + } + + err = stub.DelState(marbleName) //remove the marble from chaincode state + if err != nil { + return shim.Error("Failed to delete state:" + err.Error()) + } + + // maintain the index + indexName := "color~name" + colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marbleJSON.Color, marbleJSON.Name}) + if err != nil { + return shim.Error(err.Error()) + } + + // Delete index entry to state. + err = stub.DelState(colorNameIndexKey) + if err != nil { + return shim.Error("Failed to delete state:" + err.Error()) + } + return shim.Success(nil) +} + +// =========================================================== +// transfer a marble by setting a new owner name on the marble +// =========================================================== +func (t *SimpleChaincode) transferMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + // 0 1 + // "name", "bob" + if len(args) < 2 { + return shim.Error("Incorrect number of arguments. Expecting 2") + } + + marbleName := args[0] + newOwner := strings.ToLower(args[1]) + fmt.Println("- start transferMarble ", marbleName, newOwner) + + marbleAsBytes, err := stub.GetState(marbleName) + if err != nil { + return shim.Error("Failed to get marble:" + err.Error()) + } else if marbleAsBytes == nil { + return shim.Error("Marble does not exist") + } + + marbleToTransfer := marble{} + err = json.Unmarshal(marbleAsBytes, &marbleToTransfer) //unmarshal it aka JSON.parse() + if err != nil { + return shim.Error(err.Error()) + } + marbleToTransfer.Owner = newOwner //change the owner + + marbleJSONasBytes, _ := json.Marshal(marbleToTransfer) + err = stub.PutState(marbleName, marbleJSONasBytes) //rewrite the marble + if err != nil { + return shim.Error(err.Error()) + } + + fmt.Println("- end transferMarble (success)") + return shim.Success(nil) +} + +// =========================================================================================== +// constructQueryResponseFromIterator constructs a JSON array containing query results from +// a given result iterator +// =========================================================================================== +func constructQueryResponseFromIterator(resultsIterator shim.StateQueryIteratorInterface) (*bytes.Buffer, error) { + // buffer is a JSON array containing QueryResults + var buffer bytes.Buffer + buffer.WriteString("[") + + bArrayMemberAlreadyWritten := false + for resultsIterator.HasNext() { + queryResponse, err := resultsIterator.Next() + if err != nil { + return nil, err + } + // Add a comma before array members, suppress it for the first array member + if bArrayMemberAlreadyWritten == true { + buffer.WriteString(",") + } + buffer.WriteString("{\"Key\":") + buffer.WriteString("\"") + buffer.WriteString(queryResponse.Key) + buffer.WriteString("\"") + + buffer.WriteString(", \"Record\":") + // Record is a JSON object, so we write as-is + buffer.WriteString(string(queryResponse.Value)) + buffer.WriteString("}") + bArrayMemberAlreadyWritten = true + } + buffer.WriteString("]") + + return &buffer, nil +} + +// =========================================================================================== +// addPaginationMetadataToQueryResults adds QueryResponseMetadata, which contains pagination +// info, to the constructed query results +// =========================================================================================== +func addPaginationMetadataToQueryResults(buffer *bytes.Buffer, responseMetadata *pb.QueryResponseMetadata) *bytes.Buffer { + + buffer.WriteString("[{\"ResponseMetadata\":{\"RecordsCount\":") + buffer.WriteString("\"") + buffer.WriteString(fmt.Sprintf("%v", responseMetadata.FetchedRecordsCount)) + buffer.WriteString("\"") + buffer.WriteString(", \"Bookmark\":") + buffer.WriteString("\"") + buffer.WriteString(responseMetadata.Bookmark) + buffer.WriteString("\"}}]") + + return buffer +} + +// =========================================================================================== +// getMarblesByRange performs a range query based on the start and end keys provided. + +// Read-only function results are not typically submitted to ordering. If the read-only +// results are submitted to ordering, or if the query is used in an update transaction +// and submitted to ordering, then the committing peers will re-execute to guarantee that +// result sets are stable between endorsement time and commit time. The transaction is +// invalidated by the committing peers if the result set has changed between endorsement +// time and commit time. +// Therefore, range queries are a safe option for performing update transactions based on query results. +// =========================================================================================== +func (t *SimpleChaincode) getMarblesByRange(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + if len(args) < 2 { + return shim.Error("Incorrect number of arguments. Expecting 2") + } + + startKey := args[0] + endKey := args[1] + + resultsIterator, err := stub.GetStateByRange(startKey, endKey) + if err != nil { + return shim.Error(err.Error()) + } + defer resultsIterator.Close() + + buffer, err := constructQueryResponseFromIterator(resultsIterator) + if err != nil { + return shim.Error(err.Error()) + } + + fmt.Printf("- getMarblesByRange queryResult:\n%s\n", buffer.String()) + + return shim.Success(buffer.Bytes()) +} + +// ==== Example: GetStateByPartialCompositeKey/RangeQuery ========================================= +// transferMarblesBasedOnColor will transfer marbles of a given color to a certain new owner. +// Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'. +// Committing peers will re-execute range queries to guarantee that result sets are stable +// between endorsement time and commit time. The transaction is invalidated by the +// committing peers if the result set has changed between endorsement time and commit time. +// Therefore, range queries are a safe option for performing update transactions based on query results. +// =========================================================================================== +func (t *SimpleChaincode) transferMarblesBasedOnColor(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + // 0 1 + // "color", "bob" + if len(args) < 2 { + return shim.Error("Incorrect number of arguments. Expecting 2") + } + + color := args[0] + newOwner := strings.ToLower(args[1]) + fmt.Println("- start transferMarblesBasedOnColor ", color, newOwner) + + // Query the color~name index by color + // This will execute a key range query on all keys starting with 'color' + coloredMarbleResultsIterator, err := stub.GetStateByPartialCompositeKey("color~name", []string{color}) + if err != nil { + return shim.Error(err.Error()) + } + defer coloredMarbleResultsIterator.Close() + + // Iterate through result set and for each marble found, transfer to newOwner + var i int + for i = 0; coloredMarbleResultsIterator.HasNext(); i++ { + // Note that we don't get the value (2nd return variable), we'll just get the marble name from the composite key + responseRange, err := coloredMarbleResultsIterator.Next() + if err != nil { + return shim.Error(err.Error()) + } + + // get the color and name from color~name composite key + objectType, compositeKeyParts, err := stub.SplitCompositeKey(responseRange.Key) + if err != nil { + return shim.Error(err.Error()) + } + returnedColor := compositeKeyParts[0] + returnedMarbleName := compositeKeyParts[1] + fmt.Printf("- found a marble from index:%s color:%s name:%s\n", objectType, returnedColor, returnedMarbleName) + + // Now call the transfer function for the found marble. + // Re-use the same function that is used to transfer individual marbles + response := t.transferMarble(stub, []string{returnedMarbleName, newOwner}) + // if the transfer failed break out of loop and return error + if response.Status != shim.OK { + return shim.Error("Transfer failed: " + response.Message) + } + } + + responsePayload := fmt.Sprintf("Transferred %d %s marbles to %s", i, color, newOwner) + fmt.Println("- end transferMarblesBasedOnColor: " + responsePayload) + return shim.Success([]byte(responsePayload)) +} + +// =======Rich queries ========================================================================= +// Two examples of rich queries are provided below (parameterized query and ad hoc query). +// Rich queries pass a query string to the state database. +// Rich queries are only supported by state database implementations +// that support rich query (e.g. CouchDB). +// The query string is in the syntax of the underlying state database. +// With rich queries there is no guarantee that the result set hasn't changed between +// endorsement time and commit time, aka 'phantom reads'. +// Therefore, rich queries should not be used in update transactions, unless the +// application handles the possibility of result set changes between endorsement and commit time. +// Rich queries can be used for point-in-time queries against a peer. +// ============================================================================================ + +// ===== Example: Parameterized rich query ================================================= +// queryMarblesByOwner queries for marbles based on a passed in owner. +// This is an example of a parameterized query where the query logic is baked into the chaincode, +// and accepting a single query parameter (owner). +// Only available on state databases that support rich query (e.g. CouchDB) +// ========================================================================================= +func (t *SimpleChaincode) queryMarblesByOwner(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + // 0 + // "bob" + if len(args) < 1 { + return shim.Error("Incorrect number of arguments. Expecting 1") + } + + owner := strings.ToLower(args[0]) + + queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"marble\",\"owner\":\"%s\"}}", owner) + + queryResults, err := getQueryResultForQueryString(stub, queryString) + if err != nil { + return shim.Error(err.Error()) + } + return shim.Success(queryResults) +} + +// ===== Example: Ad hoc rich query ======================================================== +// queryMarbles uses a query string to perform a query for marbles. +// Query string matching state database syntax is passed in and executed as is. +// Supports ad hoc queries that can be defined at runtime by the client. +// If this is not desired, follow the queryMarblesForOwner example for parameterized queries. +// Only available on state databases that support rich query (e.g. CouchDB) +// ========================================================================================= +func (t *SimpleChaincode) queryMarbles(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + // 0 + // "queryString" + if len(args) < 1 { + return shim.Error("Incorrect number of arguments. Expecting 1") + } + + queryString := args[0] + + queryResults, err := getQueryResultForQueryString(stub, queryString) + if err != nil { + return shim.Error(err.Error()) + } + return shim.Success(queryResults) +} + +// ========================================================================================= +// getQueryResultForQueryString executes the passed in query string. +// Result set is built and returned as a byte array containing the JSON results. +// ========================================================================================= +func getQueryResultForQueryString(stub shim.ChaincodeStubInterface, queryString string) ([]byte, error) { + + fmt.Printf("- getQueryResultForQueryString queryString:\n%s\n", queryString) + + resultsIterator, err := stub.GetQueryResult(queryString) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + buffer, err := constructQueryResponseFromIterator(resultsIterator) + if err != nil { + return nil, err + } + + fmt.Printf("- getQueryResultForQueryString queryResult:\n%s\n", buffer.String()) + + return buffer.Bytes(), nil +} + +// ====== Pagination ========================================================================= +// Pagination provides a method to retrieve records with a defined pagesize and +// start point (bookmark). An empty string bookmark defines the first "page" of a query +// result. Paginated queries return a bookmark that can be used in +// the next query to retrieve the next page of results. Paginated queries extend +// rich queries and range queries to include a pagesize and bookmark. +// +// Two examples are provided in this example. The first is getMarblesByRangeWithPagination +// which executes a paginated range query. +// The second example is a paginated query for rich ad-hoc queries. +// ========================================================================================= + +// ====== Example: Pagination with Range Query =============================================== +// getMarblesByRangeWithPagination performs a range query based on the start & end key, +// page size and a bookmark. + +// The number of fetched records will be equal to or lesser than the page size. +// Paginated range queries are only valid for read only transactions. +// =========================================================================================== +func (t *SimpleChaincode) getMarblesByRangeWithPagination(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + if len(args) < 4 { + return shim.Error("Incorrect number of arguments. Expecting 4") + } + + startKey := args[0] + endKey := args[1] + //return type of ParseInt is int64 + pageSize, err := strconv.ParseInt(args[2], 10, 32) + if err != nil { + return shim.Error(err.Error()) + } + bookmark := args[3] + + resultsIterator, responseMetadata, err := stub.GetStateByRangeWithPagination(startKey, endKey, int32(pageSize), bookmark) + if err != nil { + return shim.Error(err.Error()) + } + defer resultsIterator.Close() + + buffer, err := constructQueryResponseFromIterator(resultsIterator) + if err != nil { + return shim.Error(err.Error()) + } + + bufferWithPaginationInfo := addPaginationMetadataToQueryResults(buffer, responseMetadata) + + fmt.Printf("- getMarblesByRange queryResult:\n%s\n", bufferWithPaginationInfo.String()) + + return shim.Success(buffer.Bytes()) +} + +// ===== Example: Pagination with Ad hoc Rich Query ======================================================== +// queryMarblesWithPagination uses a query string, page size and a bookmark to perform a query +// for marbles. Query string matching state database syntax is passed in and executed as is. +// The number of fetched records would be equal to or lesser than the specified page size. +// Supports ad hoc queries that can be defined at runtime by the client. +// If this is not desired, follow the queryMarblesForOwner example for parameterized queries. +// Only available on state databases that support rich query (e.g. CouchDB) +// Paginated queries are only valid for read only transactions. +// ========================================================================================= +func (t *SimpleChaincode) queryMarblesWithPagination(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + // 0 + // "queryString" + if len(args) < 3 { + return shim.Error("Incorrect number of arguments. Expecting 3") + } + + queryString := args[0] + //return type of ParseInt is int64 + pageSize, err := strconv.ParseInt(args[1], 10, 32) + if err != nil { + return shim.Error(err.Error()) + } + bookmark := args[2] + + queryResults, err := getQueryResultForQueryStringWithPagination(stub, queryString, int32(pageSize), bookmark) + if err != nil { + return shim.Error(err.Error()) + } + return shim.Success(queryResults) +} + +// ========================================================================================= +// getQueryResultForQueryStringWithPagination executes the passed in query string with +// pagination info. Result set is built and returned as a byte array containing the JSON results. +// ========================================================================================= +func getQueryResultForQueryStringWithPagination(stub shim.ChaincodeStubInterface, queryString string, pageSize int32, bookmark string) ([]byte, error) { + + fmt.Printf("- getQueryResultForQueryString queryString:\n%s\n", queryString) + + resultsIterator, responseMetadata, err := stub.GetQueryResultWithPagination(queryString, pageSize, bookmark) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + buffer, err := constructQueryResponseFromIterator(resultsIterator) + if err != nil { + return nil, err + } + + bufferWithPaginationInfo := addPaginationMetadataToQueryResults(buffer, responseMetadata) + + fmt.Printf("- getQueryResultForQueryString queryResult:\n%s\n", bufferWithPaginationInfo.String()) + + return buffer.Bytes(), nil +} + +func (t *SimpleChaincode) getHistoryForMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response { + + if len(args) < 1 { + return shim.Error("Incorrect number of arguments. Expecting 1") + } + + marbleName := args[0] + + fmt.Printf("- start getHistoryForMarble: %s\n", marbleName) + + resultsIterator, err := stub.GetHistoryForKey(marbleName) + if err != nil { + return shim.Error(err.Error()) + } + defer resultsIterator.Close() + + // buffer is a JSON array containing historic values for the marble + var buffer bytes.Buffer + buffer.WriteString("[") + + bArrayMemberAlreadyWritten := false + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return shim.Error(err.Error()) + } + // Add a comma before array members, suppress it for the first array member + if bArrayMemberAlreadyWritten == true { + buffer.WriteString(",") + } + buffer.WriteString("{\"TxId\":") + buffer.WriteString("\"") + buffer.WriteString(response.TxId) + buffer.WriteString("\"") + + buffer.WriteString(", \"Value\":") + // if it was a delete operation on given key, then we need to set the + //corresponding value null. Else, we will write the response.Value + //as-is (as the Value itself a JSON marble) + if response.IsDelete { + buffer.WriteString("null") + } else { + buffer.WriteString(string(response.Value)) + } + + buffer.WriteString(", \"Timestamp\":") + buffer.WriteString("\"") + buffer.WriteString(time.Unix(response.Timestamp.Seconds, int64(response.Timestamp.Nanos)).String()) + buffer.WriteString("\"") + + buffer.WriteString(", \"IsDelete\":") + buffer.WriteString("\"") + buffer.WriteString(strconv.FormatBool(response.IsDelete)) + buffer.WriteString("\"") + + buffer.WriteString("}") + bArrayMemberAlreadyWritten = true + } + buffer.WriteString("]") + + fmt.Printf("- getHistoryForMarble returning:\n%s\n", buffer.String()) + + return shim.Success(buffer.Bytes()) +} diff --git a/chaincode/marbles02/javascript/.gitignore b/chaincode/marbles02/javascript/.gitignore new file mode 100644 index 0000000..a00ca94 --- /dev/null +++ b/chaincode/marbles02/javascript/.gitignore @@ -0,0 +1,77 @@ +# +# 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/ + +# 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/chaincode/marbles02/javascript/META-INF/statedb/couchdb/indexes/indexOwner.json b/chaincode/marbles02/javascript/META-INF/statedb/couchdb/indexes/indexOwner.json new file mode 100644 index 0000000..305f090 --- /dev/null +++ b/chaincode/marbles02/javascript/META-INF/statedb/couchdb/indexes/indexOwner.json @@ -0,0 +1 @@ +{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} diff --git a/chaincode/marbles02/javascript/marbles_chaincode.js b/chaincode/marbles02/javascript/marbles_chaincode.js new file mode 100644 index 0000000..7536178 --- /dev/null +++ b/chaincode/marbles02/javascript/marbles_chaincode.js @@ -0,0 +1,481 @@ +/* +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +// ====CHAINCODE EXECUTION SAMPLES (CLI) ================== + +// ==== Invoke marbles ==== +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble1","blue","35","tom"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble2","red","50","tom"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble3","blue","70","tom"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["transferMarble","marble2","jerry"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["transferMarblesBasedOnColor","blue","jerry"]}' +// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["delete","marble1"]}' + +// ==== Query marbles ==== +// peer chaincode query -C myc1 -n marbles -c '{"Args":["readMarble","marble1"]}' +// peer chaincode query -C myc1 -n marbles -c '{"Args":["getMarblesByRange","marble1","marble3"]}' +// peer chaincode query -C myc1 -n marbles -c '{"Args":["getHistoryForMarble","marble1"]}' +// peer chaincode query -C myc1 -n marbles -c '{"Args":["getMarblesByRangeWithPagination","marble1","marble3","3",""]}' + +// Rich Query (Only supported if CouchDB is used as state database): +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarblesByOwner","tom"]}' +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarbles","{\"selector\":{\"owner\":\"tom\"}}"]}' + +// Rich Query with Pagination (Only supported if CouchDB is used as state database): +// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarblesWithPagination","{\"selector\":{\"owner\":\"tom\"}}","3",""]}' + +'use strict'; +const shim = require('fabric-shim'); +const util = require('util'); + +let Chaincode = class { + async Init(stub) { + let ret = stub.getFunctionAndParameters(); + console.info(ret); + console.info('=========== Instantiated Marbles Chaincode ==========='); + return shim.success(); + } + + async Invoke(stub) { + console.info('Transaction ID: ' + stub.getTxID()); + console.info(util.format('Args: %j', stub.getArgs())); + + let ret = stub.getFunctionAndParameters(); + console.info(ret); + + let method = this[ret.fcn]; + if (!method) { + console.log('no function of name:' + ret.fcn + ' found'); + throw new Error('Received unknown function ' + ret.fcn + ' invocation'); + } + try { + let payload = await method(stub, ret.params, this); + return shim.success(payload); + } catch (err) { + console.log(err); + return shim.error(err); + } + } + + // =============================================== + // initMarble - create a new marble + // =============================================== + async initMarble(stub, args, thisClass) { + if (args.length != 4) { + throw new Error('Incorrect number of arguments. Expecting 4'); + } + // ==== Input sanitation ==== + console.info('--- start init marble ---') + if (args[0].lenth <= 0) { + throw new Error('1st argument must be a non-empty string'); + } + if (args[1].lenth <= 0) { + throw new Error('2nd argument must be a non-empty string'); + } + if (args[2].lenth <= 0) { + throw new Error('3rd argument must be a non-empty string'); + } + if (args[3].lenth <= 0) { + throw new Error('4th argument must be a non-empty string'); + } + let marbleName = args[0]; + let color = args[1].toLowerCase(); + let owner = args[3].toLowerCase(); + let size = parseInt(args[2]); + if (typeof size !== 'number') { + throw new Error('3rd argument must be a numeric string'); + } + + // ==== Check if marble already exists ==== + let marbleState = await stub.getState(marbleName); + if (marbleState.toString()) { + throw new Error('This marble already exists: ' + marbleName); + } + + // ==== Create marble object and marshal to JSON ==== + let marble = {}; + marble.docType = 'marble'; + marble.name = marbleName; + marble.color = color; + marble.size = size; + marble.owner = owner; + + // === Save marble to state === + await stub.putState(marbleName, Buffer.from(JSON.stringify(marble))); + let indexName = 'color~name' + let colorNameIndexKey = await stub.createCompositeKey(indexName, [marble.color, marble.name]); + console.info(colorNameIndexKey); + // Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble. + // Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value + await stub.putState(colorNameIndexKey, Buffer.from('\u0000')); + // ==== Marble saved and indexed. Return success ==== + console.info('- end init marble'); + } + + // =============================================== + // readMarble - read a marble from chaincode state + // =============================================== + async readMarble(stub, args, thisClass) { + if (args.length != 1) { + throw new Error('Incorrect number of arguments. Expecting name of the marble to query'); + } + + let name = args[0]; + if (!name) { + throw new Error(' marble name must not be empty'); + } + let marbleAsbytes = await stub.getState(name); //get the marble from chaincode state + if (!marbleAsbytes.toString()) { + let jsonResp = {}; + jsonResp.Error = 'Marble does not exist: ' + name; + throw new Error(JSON.stringify(jsonResp)); + } + console.info('======================================='); + console.log(marbleAsbytes.toString()); + console.info('======================================='); + return marbleAsbytes; + } + + // ================================================== + // delete - remove a marble key/value pair from state + // ================================================== + async delete(stub, args, thisClass) { + if (args.length != 1) { + throw new Error('Incorrect number of arguments. Expecting name of the marble to delete'); + } + let marbleName = args[0]; + if (!marbleName) { + throw new Error('marble name must not be empty'); + } + // to maintain the color~name index, we need to read the marble first and get its color + let valAsbytes = await stub.getState(marbleName); //get the marble from chaincode state + let jsonResp = {}; + if (!valAsbytes) { + jsonResp.error = 'marble does not exist: ' + name; + throw new Error(jsonResp); + } + let marbleJSON = {}; + try { + marbleJSON = JSON.parse(valAsbytes.toString()); + } catch (err) { + jsonResp = {}; + jsonResp.error = 'Failed to decode JSON of: ' + marbleName; + throw new Error(jsonResp); + } + + await stub.deleteState(marbleName); //remove the marble from chaincode state + + // delete the index + let indexName = 'color~name'; + let colorNameIndexKey = stub.createCompositeKey(indexName, [marbleJSON.color, marbleJSON.name]); + if (!colorNameIndexKey) { + throw new Error(' Failed to create the createCompositeKey'); + } + // Delete index entry to state. + await stub.deleteState(colorNameIndexKey); + } + + // =========================================================== + // transfer a marble by setting a new owner name on the marble + // =========================================================== + async transferMarble(stub, args, thisClass) { + // 0 1 + // 'name', 'bob' + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting marblename and owner') + } + + let marbleName = args[0]; + let newOwner = args[1].toLowerCase(); + console.info('- start transferMarble ', marbleName, newOwner); + + let marbleAsBytes = await stub.getState(marbleName); + if (!marbleAsBytes || !marbleAsBytes.toString()) { + throw new Error('marble does not exist'); + } + let marbleToTransfer = {}; + try { + marbleToTransfer = JSON.parse(marbleAsBytes.toString()); //unmarshal + } catch (err) { + let jsonResp = {}; + jsonResp.error = 'Failed to decode JSON of: ' + marbleName; + throw new Error(jsonResp); + } + console.info(marbleToTransfer); + marbleToTransfer.owner = newOwner; //change the owner + + let marbleJSONasBytes = Buffer.from(JSON.stringify(marbleToTransfer)); + await stub.putState(marbleName, marbleJSONasBytes); //rewrite the marble + + console.info('- end transferMarble (success)'); + } + + // =========================================================================================== + // getMarblesByRange performs a range query based on the start and end keys provided. + + // Read-only function results are not typically submitted to ordering. If the read-only + // results are submitted to ordering, or if the query is used in an update transaction + // and submitted to ordering, then the committing peers will re-execute to guarantee that + // result sets are stable between endorsement time and commit time. The transaction is + // invalidated by the committing peers if the result set has changed between endorsement + // time and commit time. + // Therefore, range queries are a safe option for performing update transactions based on query results. + // =========================================================================================== + async getMarblesByRange(stub, args, thisClass) { + + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting 2'); + } + + let startKey = args[0]; + let endKey = args[1]; + + let resultsIterator = await stub.getStateByRange(startKey, endKey); + let method = thisClass['getAllResults']; + let results = await method(resultsIterator, false); + + return Buffer.from(JSON.stringify(results)); + } + + // ==== Example: GetStateByPartialCompositeKey/RangeQuery ========================================= + // transferMarblesBasedOnColor will transfer marbles of a given color to a certain new owner. + // Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'. + // Committing peers will re-execute range queries to guarantee that result sets are stable + // between endorsement time and commit time. The transaction is invalidated by the + // committing peers if the result set has changed between endorsement time and commit time. + // Therefore, range queries are a safe option for performing update transactions based on query results. + // =========================================================================================== + async transferMarblesBasedOnColor(stub, args, thisClass) { + + // 0 1 + // 'color', 'bob' + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting color and owner'); + } + + let color = args[0]; + let newOwner = args[1].toLowerCase(); + console.info('- start transferMarblesBasedOnColor ', color, newOwner); + + // Query the color~name index by color + // This will execute a key range query on all keys starting with 'color' + let coloredMarbleResultsIterator = await stub.getStateByPartialCompositeKey('color~name', [color]); + + let method = thisClass['transferMarble']; + // Iterate through result set and for each marble found, transfer to newOwner + while (true) { + let responseRange = await coloredMarbleResultsIterator.next(); + if (!responseRange || !responseRange.value || !responseRange.value.key) { + return; + } + console.log(responseRange.value.key); + + // let value = res.value.value.toString('utf8'); + let objectType; + let attributes; + ({ + objectType, + attributes + } = await stub.splitCompositeKey(responseRange.value.key)); + + let returnedColor = attributes[0]; + let returnedMarbleName = attributes[1]; + console.info(util.format('- found a marble from index:%s color:%s name:%s\n', objectType, returnedColor, returnedMarbleName)); + + // Now call the transfer function for the found marble. + // Re-use the same function that is used to transfer individual marbles + let response = await method(stub, [returnedMarbleName, newOwner]); + } + + let responsePayload = util.format('Transferred %s marbles to %s', color, newOwner); + console.info('- end transferMarblesBasedOnColor: ' + responsePayload); + } + + + // ===== Example: Parameterized rich query ================================================= + // queryMarblesByOwner queries for marbles based on a passed in owner. + // This is an example of a parameterized query where the query logic is baked into the chaincode, + // and accepting a single query parameter (owner). + // Only available on state databases that support rich query (e.g. CouchDB) + // ========================================================================================= + async queryMarblesByOwner(stub, args, thisClass) { + // 0 + // 'bob' + if (args.length < 1) { + throw new Error('Incorrect number of arguments. Expecting owner name.') + } + + let owner = args[0].toLowerCase(); + let queryString = {}; + queryString.selector = {}; + queryString.selector.docType = 'marble'; + queryString.selector.owner = owner; + let method = thisClass['getQueryResultForQueryString']; + let queryResults = await method(stub, JSON.stringify(queryString), thisClass); + return queryResults; //shim.success(queryResults); + } + + // ===== Example: Ad hoc rich query ======================================================== + // queryMarbles uses a query string to perform a query for marbles. + // Query string matching state database syntax is passed in and executed as is. + // Supports ad hoc queries that can be defined at runtime by the client. + // If this is not desired, follow the queryMarblesForOwner example for parameterized queries. + // Only available on state databases that support rich query (e.g. CouchDB) + // ========================================================================================= + async queryMarbles(stub, args, thisClass) { + // 0 + // 'queryString' + if (args.length < 1) { + throw new Error('Incorrect number of arguments. Expecting queryString'); + } + let queryString = args[0]; + if (!queryString) { + throw new Error('queryString must not be empty'); + } + let method = thisClass['getQueryResultForQueryString']; + let queryResults = await method(stub, queryString, thisClass); + return queryResults; + } + + async getAllResults(iterator, isHistory) { + let allResults = []; + while (true) { + let res = await iterator.next(); + + if (res.value && res.value.value.toString()) { + let jsonRes = {}; + console.log(res.value.value.toString('utf8')); + + if (isHistory && isHistory === true) { + jsonRes.TxId = res.value.tx_id; + jsonRes.Timestamp = res.value.timestamp; + jsonRes.IsDelete = res.value.is_delete.toString(); + try { + jsonRes.Value = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Value = res.value.value.toString('utf8'); + } + } else { + jsonRes.Key = res.value.key; + try { + jsonRes.Record = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Record = res.value.value.toString('utf8'); + } + } + allResults.push(jsonRes); + } + if (res.done) { + console.log('end of data'); + await iterator.close(); + console.info(allResults); + return allResults; + } + } + } + + // ========================================================================================= + // getQueryResultForQueryString executes the passed in query string. + // Result set is built and returned as a byte array containing the JSON results. + // ========================================================================================= + async getQueryResultForQueryString(stub, queryString, thisClass) { + + console.info('- getQueryResultForQueryString queryString:\n' + queryString) + let resultsIterator = await stub.getQueryResult(queryString); + let method = thisClass['getAllResults']; + + let results = await method(resultsIterator, false); + + return Buffer.from(JSON.stringify(results)); + } + + async getHistoryForMarble(stub, args, thisClass) { + + if (args.length < 1) { + throw new Error('Incorrect number of arguments. Expecting 1') + } + let marbleName = args[0]; + console.info('- start getHistoryForMarble: %s\n', marbleName); + + let resultsIterator = await stub.getHistoryForKey(marbleName); + let method = thisClass['getAllResults']; + let results = await method(resultsIterator, true); + + return Buffer.from(JSON.stringify(results)); + } + + // ====== Pagination ========================================================================= + // Pagination provides a method to retrieve records with a defined pagesize and + // start point (bookmark). An empty string bookmark defines the first "page" of a query + // result. Paginated queries return a bookmark that can be used in + // the next query to retrieve the next page of results. Paginated queries extend + // rich queries and range queries to include a pagesize and bookmark. + // + // Two examples are provided in this example. The first is getMarblesByRangeWithPagination + // which executes a paginated range query. + // The second example is a paginated query for rich ad-hoc queries. + // ========================================================================================= + + // ====== Example: Pagination with Range Query =============================================== + // getMarblesByRangeWithPagination performs a range query based on the start & end key, + // page size and a bookmark. + // + // The number of fetched records will be equal to or lesser than the page size. + // Paginated range queries are only valid for read only transactions. + // =========================================================================================== + async getMarblesByRangeWithPagination(stub, args, thisClass) { + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting 2'); + } + const startKey = args[0]; + const endKey = args[1]; + + const pageSize = parseInt(args[2], 10); + const bookmark = args[3]; + + const { iterator, metadata } = await stub.getStateByRangeWithPagination(startKey, endKey, pageSize, bookmark); + const getAllResults = thisClass['getAllResults']; + const results = await getAllResults(iterator, false); + // use RecordsCount and Bookmark to keep consistency with the go sample + results.ResponseMetadata = { + RecordsCount: metadata.fetched_records_count, + Bookmark: metadata.bookmark, + }; + return Buffer.from(JSON.stringify(results)); + } + + // ========================================================================================= + // getQueryResultForQueryStringWithPagination executes the passed in query string with + // pagination info. Result set is built and returned as a byte array containing the JSON results. + // ========================================================================================= + async queryMarblesWithPagination(stub, args, thisClass) { + + // 0 + // "queryString" + if (args.length < 3) { + return shim.Error("Incorrect number of arguments. Expecting 3") + } + + const queryString = args[0]; + const pageSize = parseInt(args[1], 10); + const bookmark = args[2]; + + const { iterator, metadata } = await stub.getQueryResultWithPagination(queryString, pageSize, bookmark); + const getAllResults = thisClass['getAllResults']; + const results = await getAllResults(iterator, false); + // use RecordsCount and Bookmark to keep consistency with the go sample + results.ResponseMetadata = { + RecordsCount: metadata.fetched_records_count, + Bookmark: metadata.bookmark, + }; + + return Buffer.from(JSON.stringify(results)); + } +}; + +shim.start(new Chaincode()); diff --git a/chaincode/marbles02/javascript/package.json b/chaincode/marbles02/javascript/package.json new file mode 100644 index 0000000..18e1744 --- /dev/null +++ b/chaincode/marbles02/javascript/package.json @@ -0,0 +1,17 @@ +{ + "name": "marbles", + "version": "1.0.0", + "description": "marbles chaincode implemented in node.js", + "engines": { + "node": ">=8.4.0", + "npm": ">=5.3.0" + }, + "scripts": { + "start": "node marbles_chaincode.js" + }, + "engine-strict": true, + "license": "Apache-2.0", + "dependencies": { + "fabric-shim": "^2.0.0" + } +} diff --git a/chaincode/marbles02_private/collections_config.json b/chaincode/marbles02_private/collections_config.json new file mode 100644 index 0000000..82af88d --- /dev/null +++ b/chaincode/marbles02_private/collections_config.json @@ -0,0 +1,18 @@ +[ + { + "name": "collectionMarbles", + "policy": "OR('Org1MSP.member', 'Org2MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 3, + "blockToLive":1000000, + "memberOnlyRead": true +}, + { + "name": "collectionMarblePrivateDetails", + "policy": "OR('Org1MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 3, + "blockToLive":3, + "memberOnlyRead": true + } +] diff --git a/chaincode/marbles02_private/go/META-INF/statedb/couchdb/collections/collectionMarbles/indexes/indexOwner.json b/chaincode/marbles02_private/go/META-INF/statedb/couchdb/collections/collectionMarbles/indexes/indexOwner.json new file mode 100644 index 0000000..305f090 --- /dev/null +++ b/chaincode/marbles02_private/go/META-INF/statedb/couchdb/collections/collectionMarbles/indexes/indexOwner.json @@ -0,0 +1 @@ +{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} diff --git a/chaincode/marbles02_private/go/go.mod b/chaincode/marbles02_private/go/go.mod new file mode 100644 index 0000000..8674539 --- /dev/null +++ b/chaincode/marbles02_private/go/go.mod @@ -0,0 +1,5 @@ +module github.com/hyperledger/fabric-samples/chaincode/marbles02_private/go + +go 1.13 + +require github.com/hyperledger/fabric-contract-api-go v1.1.0 diff --git a/chaincode/marbles02_private/go/go.sum b/chaincode/marbles02_private/go/go.sum new file mode 100644 index 0000000..653dbb0 --- /dev/null +++ b/chaincode/marbles02_private/go/go.sum @@ -0,0 +1,137 @@ +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 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/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/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/chaincode/marbles02_private/go/marbles_chaincode_private.go b/chaincode/marbles02_private/go/marbles_chaincode_private.go new file mode 100644 index 0000000..34d5e1b --- /dev/null +++ b/chaincode/marbles02_private/go/marbles_chaincode_private.go @@ -0,0 +1,556 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +// ====CHAINCODE EXECUTION SAMPLES (CLI) ================== + +// ==== Invoke marbles, pass private data as base64 encoded bytes in transient map ==== +// +// export MARBLE=$(echo -n "{\"name\":\"marble1\",\"color\":\"blue\",\"size\":35,\"owner\":\"tom\",\"price\":99}" | base64 | tr -d \\n) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["InitMarble"]}' --transient "{\"marble\":\"$MARBLE\"}" +// +// export MARBLE=$(echo -n "{\"name\":\"marble2\",\"color\":\"red\",\"size\":50,\"owner\":\"tom\",\"price\":102}" | base64 | tr -d \\n) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["InitMarble"]}' --transient "{\"marble\":\"$MARBLE\"}" +// +// export MARBLE=$(echo -n "{\"name\":\"marble3\",\"color\":\"blue\",\"size\":70,\"owner\":\"tom\",\"price\":103}" | base64 | tr -d \\n) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["InitMarble"]}' --transient "{\"marble\":\"$MARBLE\"}" +// +// export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"jerry\"}" | base64 | tr -d \\n) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["TransferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}" +// +// export MARBLE_DELETE=$(echo -n "{\"name\":\"marble1\"}" | base64 | tr -d \\n) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["Delete"]}' --transient "{\"marble_delete\":\"$MARBLE_DELETE\"}" + +// ==== Query marbles, since queries are not recorded on chain we don't need to hide private data in transient map ==== +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarble","marble1"]}' +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["ReadMarblePrivateDetails","marble1"]}' +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["GetMarblesByRange","marble1","marble4"]}' + +// Query a marble's public data hash +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["GetMarbleHash","collectionMarbles","marble1"]}' + +// Rich Query (Only supported if CouchDB is used as state database): +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["QueryMarblesByOwner","tom"]}' +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["QueryMarbles","{\"selector\":{\"owner\":\"tom\"}}"]}' + +// INDEXES TO SUPPORT COUCHDB RICH QUERIES +// +// Indexes in CouchDB are required in order to make JSON queries efficient and are required for +// any JSON query with a sort. As of Hyperledger Fabric 1.1, indexes may be packaged alongside +// chaincode in a META-INF/statedb/couchdb/indexes directory. Or for indexes on private data +// collections, in a META-INF/statedb/couchdb/collections//indexes directory. +// Each index must be defined in its own text file with extension *.json with the index +// definition formatted in JSON following the CouchDB index JSON syntax as documented at: +// http://docs.couchdb.org/en/2.1.1/api/database/find.html#db-index +// +// This marbles02_private example chaincode demonstrates a packaged index which you +// can find in META-INF/statedb/couchdb/collection/collectionMarbles/indexes/indexOwner.json. +// For deployment of chaincode to production environments, it is recommended +// to define any indexes alongside chaincode so that the chaincode and supporting indexes +// are deployed automatically as a unit, once the chaincode has been installed on a peer and +// instantiated on a channel. See Hyperledger Fabric documentation for more details. +// +// If you have access to the your peer's CouchDB state database in a development environment, +// you may want to iteratively test various indexes in support of your chaincode queries. You +// can use the CouchDB Fauxton interface or a command line curl utility to create and update +// indexes. Then once you finalize an index, include the index definition alongside your +// chaincode in the META-INF/statedb/couchdb/indexes directory or +// META-INF/statedb/couchdb/collections//indexes directory, for packaging +// and deployment to managed environments. +// +// In the examples below you can find index definitions that support marbles02_private +// chaincode queries, along with the syntax that you can use in development environments +// to create the indexes in the CouchDB Fauxton interface. +// + +//Example hostname:port configurations to access CouchDB. +// +//To access CouchDB docker container from within another docker container or from vagrant environments: +// http://couchdb:5984/ +// +//Inside couchdb docker container +// http://127.0.0.1:5984/ + +// Index for docType, owner. +// Note that docType and owner fields must be prefixed with the "data" wrapper +// +// Index definition for use with Fauxton interface +// {"index":{"fields":["data.docType","data.owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} + +// Index for docType, owner, size (descending order). +// Note that docType, owner and size fields must be prefixed with the "data" wrapper +// +// Index definition for use with Fauxton interface +// {"index":{"fields":[{"data.size":"desc"},{"data.docType":"desc"},{"data.owner":"desc"}]},"ddoc":"indexSizeSortDoc", "name":"indexSizeSortDesc","type":"json"} + +// Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database): +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["QueryMarbles","{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}' + +// Rich Query with index design doc specified only (Only supported if CouchDB is used as state database): +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["QueryMarbles","{\"selector\":{\"docType\":{\"$eq\":\"marble\"},\"owner\":{\"$eq\":\"tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}' + +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +type Marble struct { + ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database + Name string `json:"name"` //the fieldtags are needed to keep case from bouncing around + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` +} + +type MarblePrivateDetails struct { + ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database + Name string `json:"name"` //the fieldtags are needed to keep case from bouncing around + Price int `json:"price"` +} + +type SmartContract struct { + contractapi.Contract +} + + +// ============================================================ +// initMarble - create a new marble, store into chaincode state +// ============================================================ +func (s *SmartContract) InitMarble(ctx contractapi.TransactionContextInterface) error { + + transMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("Error getting transient: " + err.Error()) + } + + // Marble properties are private, therefore they get passed in transient field + transientMarbleJSON, ok := transMap["marble"] + if !ok { + return fmt.Errorf("marble not found in the transient map") + } + + type marbleTransientInput struct { + Name string `json:"name"` //the fieldtags are needed to keep case from bouncing around + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` + Price int `json:"price"` + } + + var marbleInput marbleTransientInput + err = json.Unmarshal(transientMarbleJSON, &marbleInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %s", err.Error()) + } + + if len(marbleInput.Name) == 0 { + return fmt.Errorf("name field must be a non-empty string") + } + if len(marbleInput.Color) == 0 { + return fmt.Errorf("color field must be a non-empty string") + } + if marbleInput.Size <= 0 { + return fmt.Errorf("size field must be a positive integer") + } + if len(marbleInput.Owner) == 0 { + return fmt.Errorf("owner field must be a non-empty string") + } + if marbleInput.Price <= 0 { + return fmt.Errorf("price field must be a positive integer") + } + + // ==== Check if marble already exists ==== + marbleAsBytes, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleInput.Name) + if err != nil { + return fmt.Errorf("Failed to get marble: " + err.Error()) + } else if marbleAsBytes != nil { + fmt.Println("This marble already exists: " + marbleInput.Name) + return fmt.Errorf("This marble already exists: " + marbleInput.Name) + } + + // ==== Create marble object, marshal to JSON, and save to state ==== + marble := &Marble{ + ObjectType: "Marble", + Name: marbleInput.Name, + Color: marbleInput.Color, + Size: marbleInput.Size, + Owner: marbleInput.Owner, + } + marbleJSONasBytes, err := json.Marshal(marble) + if err != nil { + return fmt.Errorf(err.Error()) + } + + // === Save marble to state === + err = ctx.GetStub().PutPrivateData("collectionMarbles", marbleInput.Name, marbleJSONasBytes) + if err != nil { + return fmt.Errorf("failed to put Marble: %s", err.Error()) + } + + // ==== Create marble private details object with price, marshal to JSON, and save to state ==== + marblePrivateDetails := &MarblePrivateDetails{ + ObjectType: "MarblePrivateDetails", + Name: marbleInput.Name, + Price: marbleInput.Price, + } + marblePrivateDetailsAsBytes, err := json.Marshal(marblePrivateDetails) + if err != nil { + return fmt.Errorf(err.Error()) + } + err = ctx.GetStub().PutPrivateData("collectionMarblePrivateDetails", marbleInput.Name, marblePrivateDetailsAsBytes) + if err != nil { + return fmt.Errorf("failed to put Marble private details: %s", err.Error()) + } + + // ==== Index the marble to enable color-based range queries, e.g. return all blue marbles ==== + // An 'index' is a normal key/value entry in state. + // The key is a composite key, with the elements that you want to range query on listed first. + // In our case, the composite key is based on indexName=color~name. + // This will enable very efficient state range queries based on composite keys matching indexName=color~* + indexName := "color~name" + colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(indexName, []string{marble.Color, marble.Name}) + if err != nil { + return err + } + // Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble. + // Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value + value := []byte{0x00} + err = ctx.GetStub().PutPrivateData("collectionMarbles", colorNameIndexKey, value) + + // ==== Marble saved and indexed. Return success ==== + + return nil + +} + +// =============================================== +// readMarble - read a marble from chaincode state +// =============================================== + +func (s *SmartContract) ReadMarble(ctx contractapi.TransactionContextInterface, marbleID string) (*Marble, error) { + + marbleJSON, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleID) //get the marble from chaincode state + if err != nil { + return nil, fmt.Errorf("failed to read from marble %s", err.Error()) + } + if marbleJSON == nil { + return nil, fmt.Errorf("%s does not exist", marbleID) + } + + marble := new(Marble) + _ = json.Unmarshal(marbleJSON, marble) + + return marble, nil + +} + +// =============================================== +// ReadMarblePrivateDetails - read a marble private details from chaincode state +// =============================================== +func (s *SmartContract) ReadMarblePrivateDetails(ctx contractapi.TransactionContextInterface, marbleID string) (*MarblePrivateDetails, error) { + + marbleDetailsJSON, err := ctx.GetStub().GetPrivateData("collectionMarblePrivateDetails", marbleID) //get the marble from chaincode state + if err != nil { + return nil, fmt.Errorf("failed to read from marble details %s", err.Error()) + } + if marbleDetailsJSON == nil { + return nil, fmt.Errorf("%s does not exist", marbleID) + } + + marbleDetails := new(MarblePrivateDetails) + _ = json.Unmarshal(marbleDetailsJSON, marbleDetails) + + return marbleDetails, nil +} + +// ================================================== +// delete - remove a marble key/value pair from state +// ================================================== +func (s *SmartContract) Delete(ctx contractapi.TransactionContextInterface) error { + + transMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("Error getting transient: " + err.Error()) + } + + // Marble properties are private, therefore they get passed in transient field + transientDeleteMarbleJSON, ok := transMap["marble_delete"] + if !ok { + return fmt.Errorf("marble to delete not found in the transient map") + } + + type marbleDelete struct { + Name string `json:"name"` + } + + var marbleDeleteInput marbleDelete + err = json.Unmarshal(transientDeleteMarbleJSON, &marbleDeleteInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %s", err.Error()) + } + + if len(marbleDeleteInput.Name) == 0 { + return fmt.Errorf("name field must be a non-empty string") + } + + // to maintain the color~name index, we need to read the marble first and get its color + valAsbytes, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleDeleteInput.Name) //get the marble from chaincode state + if err != nil { + return fmt.Errorf("failed to read marble: %s", err.Error()) + } + if valAsbytes == nil { + return fmt.Errorf("marble private details does not exist: %s", marbleDeleteInput.Name) + } + + var marbleToDelete Marble + err = json.Unmarshal([]byte(valAsbytes), &marbleToDelete) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %s", err.Error()) + } + + // delete the marble from state + err = ctx.GetStub().DelPrivateData("collectionMarbles", marbleDeleteInput.Name) + if err != nil { + return fmt.Errorf("Failed to delete state:" + err.Error()) + } + + // Also delete the marble from the color~name index + indexName := "color~name" + colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(indexName, []string{marbleToDelete.Color, marbleToDelete.Name}) + if err != nil { + return err + } + err = ctx.GetStub().DelPrivateData("collectionMarbles", colorNameIndexKey) + if err != nil { + return fmt.Errorf("Failed to delete marble:" + err.Error()) + } + + // Finally, delete private details of marble + err = ctx.GetStub().DelPrivateData("collectionMarblePrivateDetails", marbleDeleteInput.Name) + if err != nil { + return err + } + + return nil + +} + +// =========================================================== +// transfer a marble by setting a new owner name on the marble +// =========================================================== +func (s *SmartContract) TransferMarble(ctx contractapi.TransactionContextInterface) error { + + transMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("Error getting transient: " + err.Error()) + } + + // Marble properties are private, therefore they get passed in transient field + transientTransferMarbleJSON, ok := transMap["marble_owner"] + if !ok { + return fmt.Errorf("marble owner not found in the transient map") + } + + type marbleTransferTransientInput struct { + Name string `json:"name"` + Owner string `json:"owner"` + } + + var marbleTransferInput marbleTransferTransientInput + err = json.Unmarshal(transientTransferMarbleJSON, &marbleTransferInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %s", err.Error()) + } + + + if len(marbleTransferInput.Name) == 0 { + return fmt.Errorf("name field must be a non-empty string") + } + if len(marbleTransferInput.Owner) == 0 { + return fmt.Errorf("owner field must be a non-empty string") + } + + marbleAsBytes, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleTransferInput.Name) + if err != nil { + return fmt.Errorf("Failed to get marble:" + err.Error()) + } else if marbleAsBytes == nil { + return fmt.Errorf("Marble does not exist: " + marbleTransferInput.Name) + } + + marbleToTransfer := Marble{} + err = json.Unmarshal(marbleAsBytes, &marbleToTransfer) //unmarshal it aka JSON.parse() + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %s", err.Error()) + } + + marbleToTransfer.Owner = marbleTransferInput.Owner //change the owner + + marbleJSONasBytes, _ := json.Marshal(marbleToTransfer) + err = ctx.GetStub().PutPrivateData("collectionMarbles", marbleToTransfer.Name, marbleJSONasBytes) //rewrite the marble + if err != nil { + return err + } + + return nil + +} + +// =========================================================================================== +// getMarblesByRange performs a range query based on the start and end keys provided. + +// Read-only function results are not typically submitted to ordering. If the read-only +// results are submitted to ordering, or if the query is used in an update transaction +// and submitted to ordering, then the committing peers will re-execute to guarantee that +// result sets are stable between endorsement time and commit time. The transaction is +// invalidated by the committing peers if the result set has changed between endorsement +// time and commit time. +// Therefore, range queries are a safe option for performing update transactions based on query results. +// =========================================================================================== +func (s *SmartContract) GetMarblesByRange(ctx contractapi.TransactionContextInterface, startKey string, endKey string) ([]Marble, error) { + + resultsIterator, err := ctx.GetStub().GetPrivateDataByRange("collectionMarbles", startKey, endKey) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []Marble{} + + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + newMarble := new(Marble) + + err = json.Unmarshal(response.Value, newMarble) + if err != nil { + return nil, err + } + + results = append(results, *newMarble) + } + + return results, nil + +} + + +// =======Rich queries ========================================================================= +// Two examples of rich queries are provided below (parameterized query and ad hoc query). +// Rich queries pass a query string to the state database. +// Rich queries are only supported by state database implementations +// that support rich query (e.g. CouchDB). +// The query string is in the syntax of the underlying state database. +// With rich queries there is no guarantee that the result set hasn't changed between +// endorsement time and commit time, aka 'phantom reads'. +// Therefore, rich queries should not be used in update transactions, unless the +// application handles the possibility of result set changes between endorsement and commit time. +// Rich queries can be used for point-in-time queries against a peer. +// ============================================================================================ + +// ===== Example: Parameterized rich query ================================================= +// queryMarblesByOwner queries for marbles based on a passed in owner. +// This is an example of a parameterized query where the query logic is baked into the chaincode, +// and accepting a single query parameter (owner). +// Only available on state databases that support rich query (e.g. CouchDB) +// ========================================================================================= +func (s *SmartContract) QueryMarblesByOwner(ctx contractapi.TransactionContextInterface, owner string) ([]Marble, error) { + + ownerString := strings.ToLower(owner) + + queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"marble\",\"owner\":\"%s\"}}", ownerString) + + queryResults, err := s.getQueryResultForQueryString(ctx, queryString) + if err != nil { + return nil, err + } + return queryResults, nil +} + +// ===== Example: Ad hoc rich query ======================================================== +// queryMarbles uses a query string to perform a query for marbles. +// Query string matching state database syntax is passed in and executed as is. +// Supports ad hoc queries that can be defined at runtime by the client. +// If this is not desired, follow the queryMarblesForOwner example for parameterized queries. +// Only available on state databases that support rich query (e.g. CouchDB) +// ========================================================================================= +func (s *SmartContract) QueryMarbles(ctx contractapi.TransactionContextInterface, queryString string) ([]Marble, error) { + + queryResults, err := s.getQueryResultForQueryString(ctx, queryString) + if err != nil { + return nil, err + } + return queryResults, nil +} + +// ========================================================================================= +// getQueryResultForQueryString executes the passed in query string. +// Result set is built and returned as a byte array containing the JSON results. +// ========================================================================================= +func (s *SmartContract) getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string) ([]Marble, error) { + + resultsIterator, err := ctx.GetStub().GetPrivateDataQueryResult("collectionMarbles", queryString) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []Marble{} + + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + newMarble := new(Marble) + + err = json.Unmarshal(response.Value, newMarble) + if err != nil { + return nil, err + } + + results = append(results, *newMarble) + } + return results, nil +} + +// =============================================== +// getMarbleHash - use the public data hash to verify a private marble +// Result is the hash on the public ledger of a marble stored a private data collection +// =============================================== +func (s *SmartContract) GetMarbleHash(ctx contractapi.TransactionContextInterface, collection string, marbleID string,) (string, error) { + + // GetPrivateDataHash can use any collection deployed with the chaincode as input + hashAsbytes, err := ctx.GetStub().GetPrivateDataHash(collection, marbleID) + if err != nil { + return "", fmt.Errorf("Failed to get public data hash for marble:" + err.Error()) + } else if hashAsbytes == nil { + return "", fmt.Errorf("Marble does not exist: " + marbleID) + } + + return string(hashAsbytes), nil +} + +func main() { + + chaincode, err := contractapi.NewChaincode(new(SmartContract)) + + if err != nil { + fmt.Printf("Error creating private mables chaincode: %s", err.Error()) + return + } + + if err := chaincode.Start(); err != nil { + fmt.Printf("Error starting private mables chaincode: %s", err.Error()) + } +} diff --git a/chaincode/sacc/go.mod b/chaincode/sacc/go.mod new file mode 100644 index 0000000..f9deb8a --- /dev/null +++ b/chaincode/sacc/go.mod @@ -0,0 +1,12 @@ +module github.com/hyperledger/fabric-samples/chaincode/sacc + +go 1.12 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 + github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect + golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + golang.org/x/text v0.3.2 // indirect + google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect +) diff --git a/chaincode/sacc/go.sum b/chaincode/sacc/go.sum new file mode 100644 index 0000000..da5b571 --- /dev/null +++ b/chaincode/sacc/go.sum @@ -0,0 +1,82 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +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/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 h1:VEm3tPRTCzq3J/1XpVERh1PbOSnshUVwx2G5s3cLiTw= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85/go.mod h1:HZK6PKLWrvdD/t0oSLiyaRaUM6fZ7qjJuOlb0zrn0mo= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 h1:WzttYAPO5xkQ87ZrxzEhvDZknfarSNu1PZt3NPMTE3Y= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/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-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-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +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/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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/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= diff --git a/chaincode/sacc/sacc.go b/chaincode/sacc/sacc.go new file mode 100644 index 0000000..5fffe97 --- /dev/null +++ b/chaincode/sacc/sacc.go @@ -0,0 +1,97 @@ +/* + * Copyright IBM Corp All Rights Reserved + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "fmt" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-protos-go/peer" +) + +// SimpleAsset implements a simple chaincode to manage an asset +type SimpleAsset struct { +} + +// Init is called during chaincode instantiation to initialize any +// data. Note that chaincode upgrade also calls this function to reset +// or to migrate data. +func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { + // Get the args from the transaction proposal + args := stub.GetStringArgs() + if len(args) != 2 { + return shim.Error("Incorrect arguments. Expecting a key and a value") + } + + // Set up any variables or assets here by calling stub.PutState() + + // We store the key and the value on the ledger + err := stub.PutState(args[0], []byte(args[1])) + if err != nil { + return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0])) + } + return shim.Success(nil) +} + +// Invoke is called per transaction on the chaincode. Each transaction is +// either a 'get' or a 'set' on the asset created by Init function. The Set +// method may create a new asset by specifying a new key-value pair. +func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response { + // Extract the function and args from the transaction proposal + fn, args := stub.GetFunctionAndParameters() + + var result string + var err error + if fn == "set" { + result, err = set(stub, args) + } else { // assume 'get' even if fn is nil + result, err = get(stub, args) + } + if err != nil { + return shim.Error(err.Error()) + } + + // Return the result as success payload + return shim.Success([]byte(result)) +} + +// Set stores the asset (both key and value) on the ledger. If the key exists, +// it will override the value with the new one +func set(stub shim.ChaincodeStubInterface, args []string) (string, error) { + if len(args) != 2 { + return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value") + } + + err := stub.PutState(args[0], []byte(args[1])) + if err != nil { + return "", fmt.Errorf("Failed to set asset: %s", args[0]) + } + return args[1], nil +} + +// Get returns the value of the specified asset key +func get(stub shim.ChaincodeStubInterface, args []string) (string, error) { + if len(args) != 1 { + return "", fmt.Errorf("Incorrect arguments. Expecting a key") + } + + value, err := stub.GetState(args[0]) + if err != nil { + return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err) + } + if value == nil { + return "", fmt.Errorf("Asset not found: %s", args[0]) + } + return string(value), nil +} + +// main function starts up the chaincode in the container during instantiate +func main() { + if err := shim.Start(new(SimpleAsset)); err != nil { + fmt.Printf("Error starting SimpleAsset chaincode: %s", err) + } +} diff --git a/chaincode/sacc/sacc_test.go b/chaincode/sacc/sacc_test.go new file mode 100644 index 0000000..7ab39c9 --- /dev/null +++ b/chaincode/sacc/sacc_test.go @@ -0,0 +1,174 @@ +/* +Copyright Hitachi America Ltd. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "fmt" + "testing" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-chaincode-go/shimtest" +) + +func checkInit(t *testing.T, stub *shimtest.MockStub, args [][]byte) { + res := stub.MockInit("1", args) + if res.Status != shim.OK { + fmt.Println("Init failed", string(res.Message)) + t.FailNow() + } +} + +func checkState(t *testing.T, stub *shimtest.MockStub, name string, value string) { + bytes := stub.State[name] + if bytes == nil { + fmt.Println("State", name, "failed to get value") + t.FailNow() + } + if string(bytes) != value { + fmt.Println("State value", name, "was not", value, "as expected") + t.FailNow() + } +} + +func checkQuery(t *testing.T, stub *shimtest.MockStub, name string, value string) { + res := stub.MockInvoke("1", [][]byte{[]byte("query"), []byte(name)}) + if res.Status != shim.OK { + fmt.Println("Query", name, "failed", string(res.Message)) + t.FailNow() + } + if res.Payload == nil { + fmt.Println("Query", name, "failed to get value") + t.FailNow() + } + if string(res.Payload) != value { + fmt.Println("Query value", name, "was not", value, "as expected") + t.FailNow() + } +} + +func checkInvoke(t *testing.T, stub *shimtest.MockStub, args [][]byte) { + res := stub.MockInvoke("1", args) + if res.Status != shim.OK { + fmt.Println("Invoke", args, "failed", string(res.Message)) + t.FailNow() + } +} + +func TestSacc_Init(t *testing.T) { + cc := new(SimpleAsset) + stub := shimtest.NewMockStub("sacc", cc) + + // Init a=10 + checkInit(t, stub, [][]byte{[]byte("a"), []byte("10")}) + + checkState(t, stub, "a", "10") +} + +func TestSacc_Query(t *testing.T) { + cc := new(SimpleAsset) + stub := shimtest.NewMockStub("sacc", cc) + + // Init a=10 + checkInit(t, stub, [][]byte{[]byte("a"), []byte("10")}) + + // Query a + checkQuery(t, stub, "a", "10") +} + +func TestSacc_Invoke(t *testing.T) { + cc := new(SimpleAsset) + stub := shimtest.NewMockStub("sacc", cc) + + // Init a=10 + checkInit(t, stub, [][]byte{[]byte("a"), []byte("10")}) + + // Invoke: Set a=20 + checkInvoke(t, stub, [][]byte{[]byte("set"), []byte("a"), []byte("20")}) + + // Query a + checkQuery(t, stub, "a", "20") +} + +func TestSacc_InitWithIncorrectArguments(t *testing.T) { + cc := new(SimpleAsset) + stub := shimtest.NewMockStub("sacc", cc) + + // Init with incorrect arguments + res := stub.MockInit("1", [][]byte{[]byte("a"), []byte("10"), []byte("10")}) + + if res.Status != shim.ERROR { + fmt.Println("Invalid Init accepted") + t.FailNow() + } + + if res.Message != "Incorrect arguments. Expecting a key and a value" { + fmt.Println("Unexpected Error message:", string(res.Message)) + t.FailNow() + } +} + +func TestSacc_QueryWithIncorrectArguments(t *testing.T) { + cc := new(SimpleAsset) + stub := shimtest.NewMockStub("sacc", cc) + + // Init a=10 + checkInit(t, stub, [][]byte{[]byte("a"), []byte("10")}) + + // Query with incorrect arguments + res := stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("a"), []byte("b")}) + + if res.Status != shim.ERROR { + fmt.Println("Invalid query accepted") + t.FailNow() + } + + if res.Message != "Incorrect arguments. Expecting a key" { + fmt.Println("Unexpected Error message:", string(res.Message)) + t.FailNow() + } +} + +func TestSacc_QueryForAssetNotFound(t *testing.T) { + cc := new(SimpleAsset) + stub := shimtest.NewMockStub("sacc", cc) + + // Init a=10 + checkInit(t, stub, [][]byte{[]byte("a"), []byte("10")}) + + // Query for b (as a asset not found) + res := stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("b")}) + + if res.Status != shim.ERROR { + fmt.Println("Invalid query accepted") + t.FailNow() + } + + if res.Message != "Asset not found: b" { + fmt.Println("Unexpected Error message:", string(res.Message)) + t.FailNow() + } +} + +func TestSacc_InvokeWithIncorrectArguments(t *testing.T) { + cc := new(SimpleAsset) + stub := shimtest.NewMockStub("sacc", cc) + + // Init a=10 + checkInit(t, stub, [][]byte{[]byte("a"), []byte("10")}) + + // Invoke with incorrect arguments + res := stub.MockInvoke("1", [][]byte{[]byte("set"), []byte("a")}) + if res.Status != shim.ERROR { + fmt.Println("Invalid Invoke accepted") + t.FailNow() + } + + if res.Message != "Incorrect arguments. Expecting a key and a value" { + fmt.Println("Unexpected Error message:", string(res.Message)) + t.FailNow() + } +} diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml new file mode 100644 index 0000000..5d314a8 --- /dev/null +++ b/ci/azure-pipelines.yml @@ -0,0 +1,255 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +trigger: + - master + - release-1.4 + +variables: + FABRIC_VERSION: 2.2 + GO_BIN: $(Build.Repository.LocalPath)/bin + GO_VER: 1.14.6 + NODE_VER: 12.x + PATH: $(Build.Repository.LocalPath)/bin:/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin + +jobs: + - job: CommercialPaper_Go + displayName: Commercial Paper (Go) + pool: + vmImage: ubuntu-18.04 + steps: + - template: templates/install-deps.yml + - template: templates/commercial-paper/azure-pipelines-go.yml + + - job: CommercialPaper_Java + displayName: Commercial Paper (Java) + pool: + vmImage: ubuntu-18.04 + steps: + - template: templates/install-deps.yml + - template: templates/commercial-paper/azure-pipelines-java.yml + + - job: CommercialPaper_JavaScript + displayName: Commercial Paper (JavaScript) + pool: + vmImage: ubuntu-18.04 + steps: + - template: templates/install-deps.yml + - template: templates/commercial-paper/azure-pipelines-javascript.yml + + - job: FabCar_Go + displayName: FabCar (Go) + pool: + vmImage: ubuntu-18.04 + steps: + - template: templates/install-deps.yml + - template: templates/fabcar/azure-pipelines-go.yml + + - job: FabCar_Java + displayName: FabCar (Java) + pool: + vmImage: ubuntu-18.04 + steps: + - template: templates/install-deps.yml + - template: templates/fabcar/azure-pipelines-java.yml + + - job: FabCar_JavaScript + displayName: FabCar (JavaScript) + pool: + vmImage: ubuntu-18.04 + steps: + - template: templates/install-deps.yml + - template: templates/fabcar/azure-pipelines-javascript.yml + + - job: Fabcar_TypeScript + displayName: FabCar (TypeScript) + pool: + vmImage: ubuntu-18.04 + steps: + - template: templates/install-deps.yml + - template: templates/fabcar/azure-pipelines-typescript.yml + + - job: Lint + displayName: Lint + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + Basic-Application-Go: + DIRECTORY: asset-transfer-basic + LANGUAGE: go + TYPE: application + Basic-Application-Java: + DIRECTORY: asset-transfer-basic + LANGUAGE: java + TYPE: application + Basic-Application-Javascript: + DIRECTORY: asset-transfer-basic + LANGUAGE: javascript + TYPE: application + Basic-Chaincode-Go: + DIRECTORY: asset-transfer-basic + LANGUAGE: go + TYPE: chaincode + Basic-Chaincode-Java: + DIRECTORY: asset-transfer-basic + LANGUAGE: java + TYPE: chaincode + Basic-Chaincode-Javascript: + DIRECTORY: asset-transfer-basic + LANGUAGE: javascript + TYPE: chaincode + Basic-Chaincode-Typescript: + DIRECTORY: asset-transfer-basic + LANGUAGE: typescript + TYPE: chaincode + Ledger-Application-Java: + DIRECTORY: asset-transfer-ledger-queries + LANGUAGE: java + TYPE: application + Ledger-Chaincode-Go: + DIRECTORY: asset-transfer-ledger-queries + LANGUAGE: go + TYPE: chaincode + Ledger-Chaincode-Javascript: + DIRECTORY: asset-transfer-ledger-queries + LANGUAGE: javascript + TYPE: chaincode + PrivateData-Application-Javascript: + DIRECTORY: asset-transfer-private-data + LANGUAGE: javascript + TYPE: application + PrivateData-Chaincode-Go: + DIRECTORY: asset-transfer-private-data + LANGUAGE: go + TYPE: chaincode + SBE-Chaincode-Typescript: + DIRECTORY: asset-transfer-sbe + LANGUAGE: typescript + TYPE: chaincode + SBE-Chaincode-Java: + DIRECTORY: asset-transfer-sbe + LANGUAGE: java + TYPE: chaincode + Secured-Chaincode-Go: + DIRECTORY: asset-transfer-secured-agreement + LANGUAGE: go + TYPE: chaincode + steps: + - task: GoTool@0 + inputs: + goBin: $(GO_BIN) + version: $(GO_VER) + displayName: Install GoLang + - task: NodeTool@0 + inputs: + versionSpec: $(NODE_VER) + displayName: Install Node.js + - script: ./ci/scripts/lint.sh + displayName: Lint Code + + - job: TestNetworkBasic + displayName: Test Network + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + Basic-Go: + CHAINCODE_NAME: basic + CHAINCODE_LANGUAGE: go + Basic-Java: + CHAINCODE_NAME: basic + CHAINCODE_LANGUAGE: java + Basic-Javascript: + CHAINCODE_NAME: basic + CHAINCODE_LANGUAGE: javascript + Basic-Typescript: + CHAINCODE_NAME: basic + CHAINCODE_LANGUAGE: typescript + steps: + - template: templates/install-deps.yml + - script: ../ci/scripts/run-test-network-basic.sh + workingDirectory: test-network + displayName: Run Test Network Basic Chaincode + + - job: TestNetworkLedger + displayName: Test Network + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + Ledger-Go: + CHAINCODE_NAME: ledger + CHAINCODE_LANGUAGE: go + Ledger-Javascript: + CHAINCODE_NAME: ledger + CHAINCODE_LANGUAGE: javascript + steps: + - template: templates/install-deps.yml + - script: ../ci/scripts/run-test-network-ledger.sh + workingDirectory: test-network + displayName: Run Test Network Ledger Chaincode + + - job: TestNetworkPrivate + displayName: Test Network + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + Private-Go: + CHAINCODE_NAME: private + CHAINCODE_LANGUAGE: go + steps: + - template: templates/install-deps.yml + - script: ../ci/scripts/run-test-network-private.sh + workingDirectory: test-network + displayName: Run Test Network Private Chaincode + + - job: TestNetworkSBE + displayName: Test Network + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + SBE-Typescript: + CHAINCODE_NAME: sbe + CHAINCODE_LANGUAGE: typescript + SBE-Java: + CHAINCODE_NAME: sbe + CHAINCODE_LANGUAGE: java + steps: + - template: templates/install-deps.yml + - script: ../ci/scripts/run-test-network-sbe.sh + workingDirectory: test-network + displayName: Run Test Network SBE Chaincode + + - job: TestNetworkSecured + displayName: Test Network + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + Secured-Go: + CHAINCODE_NAME: secured + CHAINCODE_LANGUAGE: go + steps: + - template: templates/install-deps.yml + - 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/lint.sh b/ci/scripts/lint.sh new file mode 100755 index 0000000..275b4d6 --- /dev/null +++ b/ci/scripts/lint.sh @@ -0,0 +1,51 @@ +set -euo pipefail + +function print() { + GREEN='\033[0;32m' + NC='\033[0m' + echo + echo -e "${GREEN}${1}${NC}" +} + +if [[ "${LANGUAGE}" == "go" ]]; then + go get golang.org/x/tools/cmd/goimports + + cd "${DIRECTORY}/${TYPE}-${LANGUAGE}" + print "Running go vet" + go vet ./... + + print "Running gofmt" + output=$(gofmt -l -s $(go list -f '{{.Dir}}' ./...)) + if [[ "${output}" != "" ]]; then + print "The following files contain formatting errors, please run 'gofmt -l -w ' to fix these issues:" + echo "${output}" + fi + + print "Running goimports" + output=$(goimports -l $(go list -f '{{.Dir}}' ./...)) + if [[ "${output}" != "" ]]; then + print "The following files contain import errors, please run 'goimports -l -w ' to fix these issues:" + echo "${output}" + fi +elif [[ "${LANGUAGE}" == "java" ]]; then + cd "${DIRECTORY}/${TYPE}-${LANGUAGE}" + print "Running Gradle Build" + ./gradlew build +elif [[ "${LANGUAGE}" == "javascript" ]]; then + npm install -g eslint + cd "${DIRECTORY}/${TYPE}-${LANGUAGE}" + print "Running ESLint" + if [[ "${TYPE}" == "chaincode" ]]; then + eslint *.js */**.js + else + eslint *.js + fi +elif [[ "${LANGUAGE}" == "typescript" ]]; then + npm install -g typescript tslint + cd "${DIRECTORY}/${TYPE}-${LANGUAGE}" + print "Running TSLint" + tslint --project . +else + echo "Language not supported" + exit 1 +fi diff --git a/ci/scripts/pullFabricImages.sh b/ci/scripts/pullFabricImages.sh new file mode 100755 index 0000000..38cb8e2 --- /dev/null +++ b/ci/scripts/pullFabricImages.sh @@ -0,0 +1,15 @@ +#!/bin/bash -e +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +STABLE_TAG=amd64-${FABRIC_VERSION}-stable + +for image in baseos peer orderer ca tools orderer ccenv javaenv nodeenv tools; do + docker pull -q "hyperledger-fabric.jfrog.io/fabric-${image}:${STABLE_TAG}" + docker tag "hyperledger-fabric.jfrog.io/fabric-${image}:${STABLE_TAG}" hyperledger/fabric-${image} + docker tag "hyperledger-fabric.jfrog.io/fabric-${image}:${STABLE_TAG}" "hyperledger/fabric-${image}:${FABRIC_VERSION}" + docker rmi -f "hyperledger-fabric.jfrog.io/fabric-${image}:${STABLE_TAG}" +done + +docker pull -q couchdb:3.1.1 +docker images | grep hyperledger diff --git a/ci/scripts/run-test-network-basic.sh b/ci/scripts/run-test-network-basic.sh new file mode 100755 index 0000000..6cda208 --- /dev/null +++ b/ci/scripts/run-test-network-basic.sh @@ -0,0 +1,65 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-go} +CHAINCODE_NAME=${CHAINCODE_NAME:-basic} +CHAINCODE_PATH=${CHAINCODE_PATH:-../asset-transfer-basic} + +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 -s couchdb -i "${FABRIC_VERSION}" + print "Deploying ${CHAINCODE_NAME} chaincode" + ./network.sh deployCC -ccn "${CHAINCODE_NAME}" -ccp "${CHAINCODE_PATH}/chaincode-${CHAINCODE_LANGUAGE}" -ccv 1 -ccs 1 -ccl "${CHAINCODE_LANGUAGE}" +} + +function stopNetwork() { + print "Stopping network" + ./network.sh down +} + +# Run Go application +createNetwork +print "Initializing Go application" +pushd ../asset-transfer-basic/application-go +print "Executing AssetTransfer.go" +go run . +popd +stopNetwork + +# Run Java application +createNetwork +print "Initializing Java application" +pushd ../asset-transfer-basic/application-java +print "Executing Gradle Run" +gradle run +popd +stopNetwork + +# Run Javascript application +createNetwork +print "Initializing Javascript application" +pushd ../asset-transfer-basic/application-javascript +npm install +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 diff --git a/ci/scripts/run-test-network-events.sh b/ci/scripts/run-test-network-events.sh new file mode 100755 index 0000000..770f023 --- /dev/null +++ b/ci/scripts/run-test-network-events.sh @@ -0,0 +1,37 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-javascript} +CHAINCODE_NAME=${CHAINCODE_NAME:-events} +CHAINCODE_PATH=${CHAINCODE_PATH:-../asset-transfer-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}" -ccp "${CHAINCODE_PATH}/chaincode-${CHAINCODE_LANGUAGE}" -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/ci/scripts/run-test-network-ledger.sh b/ci/scripts/run-test-network-ledger.sh new file mode 100755 index 0000000..6546833 --- /dev/null +++ b/ci/scripts/run-test-network-ledger.sh @@ -0,0 +1,44 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-go} +CHAINCODE_NAME=${CHAINCODE_NAME:-ledger} +CHAINCODE_PATH=${CHAINCODE_PATH:-../asset-transfer-ledger-queries} + +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 -s couchdb -i "${FABRIC_VERSION}" + print "Deploying ${CHAINCODE_NAME} chaincode" + ./network.sh deployCC -ccn "${CHAINCODE_NAME}" -ccp "${CHAINCODE_PATH}/chaincode-${CHAINCODE_LANGUAGE}" -ccv 1 -ccs 1 -ccl "${CHAINCODE_LANGUAGE}" +} + +function stopNetwork() { + print "Stopping network" + ./network.sh down +} + +# Run Java application +createNetwork +print "Initializing Java application" +pushd ../asset-transfer-ledger-queries/application-java +print "Executing Gradle Run" +gradle run +popd +stopNetwork + +# Run Javascript application +createNetwork +print "Initializing Javascript application" +pushd ../asset-transfer-ledger-queries/application-javascript +npm install +print "Executing app.js" +node app.js +popd +stopNetwork diff --git a/ci/scripts/run-test-network-private.sh b/ci/scripts/run-test-network-private.sh new file mode 100755 index 0000000..19dff1a --- /dev/null +++ b/ci/scripts/run-test-network-private.sh @@ -0,0 +1,35 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-go} +CHAINCODE_NAME=${CHAINCODE_NAME:-private} +CHAINCODE_PATH=${CHAINCODE_PATH:-../asset-transfer-private-data} + +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 -s couchdb -i "${FABRIC_VERSION}" + print "Deploying ${CHAINCODE_NAME} chaincode" + ./network.sh deployCC -ccn "${CHAINCODE_NAME}" -ccp "${CHAINCODE_PATH}/chaincode-${CHAINCODE_LANGUAGE}" -ccv 1 -ccs 1 -ccl "${CHAINCODE_LANGUAGE}" -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cccg ../asset-transfer-private-data/chaincode-go/collections_config.json +} + +function stopNetwork() { + print "Stopping network" + ./network.sh down +} + +# Run Javascript application +createNetwork +print "Initializing Javascript application" +pushd ../asset-transfer-private-data/application-javascript +npm install +print "Executing app.js" +node app.js +popd +stopNetwork diff --git a/ci/scripts/run-test-network-sbe.sh b/ci/scripts/run-test-network-sbe.sh new file mode 100755 index 0000000..c5ccfec --- /dev/null +++ b/ci/scripts/run-test-network-sbe.sh @@ -0,0 +1,35 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-typescript} +CHAINCODE_NAME=${CHAINCODE_NAME:-sbe} +CHAINCODE_PATH=${CHAINCODE_PATH:-../asset-transfer-sbe} + +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 -s couchdb -i "${FABRIC_VERSION}" + print "Deploying ${CHAINCODE_NAME} chaincode" + ./network.sh deployCC -ccn "${CHAINCODE_NAME}" -ccp "${CHAINCODE_PATH}/chaincode-${CHAINCODE_LANGUAGE}" -ccv 1 -ccs 1 -ccl "${CHAINCODE_LANGUAGE}" +} + +function stopNetwork() { + print "Stopping network" + ./network.sh down +} + +# Run Javascript application +createNetwork +print "Initializing Javascript application" +pushd ../asset-transfer-sbe/application-javascript +npm install +print "Executing app.js" +node app.js +popd +stopNetwork diff --git a/ci/scripts/run-test-network-secured.sh b/ci/scripts/run-test-network-secured.sh new file mode 100755 index 0000000..6fb6a84 --- /dev/null +++ b/ci/scripts/run-test-network-secured.sh @@ -0,0 +1,37 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-go} +CHAINCODE_NAME=${CHAINCODE_NAME:-secured} +CHAINCODE_PATH=${CHAINCODE_PATH:-../asset-transfer-secured-agreement} + +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}" -ccp "${CHAINCODE_PATH}/chaincode-${CHAINCODE_LANGUAGE}" -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-secured-agreement/application-javascript +npm install +print "Executing app.js" +node app.js +popd +stopNetwork +print "Remove wallet storage" +rm -R ../asset-transfer-secured-agreement/application-javascript/wallet diff --git a/ci/templates/commercial-paper/azure-pipelines-go.yml b/ci/templates/commercial-paper/azure-pipelines-go.yml new file mode 100644 index 0000000..bb50ebd --- /dev/null +++ b/ci/templates/commercial-paper/azure-pipelines-go.yml @@ -0,0 +1,97 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - script: go test ./... + workingDirectory: commercial-paper/organization/magnetocorp/contract-go + displayName: Unit Test MagnetoCorp Chaincode + - script: go test ./... + workingDirectory: commercial-paper/organization/digibank/contract-go + displayName: Unit Test DigiBank Chaincode + + - script: go mod vendor + workingDirectory: commercial-paper/organization/magnetocorp/contract-go + displayName: Vendor MagnetoCorp Dependencies + - script: go mod vendor + workingDirectory: commercial-paper/organization/digibank/contract-go + displayName: Vendor DigiBank Dependencies + + - script: | + ./network.sh up createChannel -ca -s couchdb -i ${FABRIC_VERSION} # FABRIC_VERSION is set in ci/azure-pipelines.yml + + # Copy the connection profiles so they are in the correct organizations. + cp "./organizations/peerOrganizations/org1.example.com/connection-org1.yaml" "../commercial-paper/organization/digibank/gateway/" + cp "./organizations/peerOrganizations/org2.example.com/connection-org2.yaml" "../commercial-paper/organization/magnetocorp/gateway/" + workingDirectory: test-network + displayName: Start Fabric + - script: | + source <(./magnetocorp.sh) + peer lifecycle chaincode package cp.tar.gz --lang golang --path ./contract-go --label cp_0 + peer lifecycle chaincode install cp.tar.gz + + export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') + echo $PACKAGE_ID + + peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + + peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + workingDirectory: commercial-paper/organization/magnetocorp + displayName: Setup Commercial Paper Contract + - script: | + source <(./digibank.sh) + peer lifecycle chaincode package cp.tar.gz --lang golang --path ./contract-go --label cp_0 + peer lifecycle chaincode install cp.tar.gz + + export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') + echo $PACKAGE_ID + + peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + + peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + + peer lifecycle chaincode commit -o localhost:7050 \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel --name papercontract -v 0 \ + --sequence 1 \ + --tls --cafile $ORDERER_CA --waitForEvent + + workingDirectory: commercial-paper/organization/digibank + displayName: Setup Commercial Paper Contract + + - script: retry -- npm install + workingDirectory: commercial-paper/organization/magnetocorp/application + displayName: Install Magnetocorp Application + - script: | + set -ex + node enrollUser.js + node issue.js + workingDirectory: commercial-paper/organization/magnetocorp/application + displayName: MagnetoCorp Issue Paper + + - script: retry -- npm install + workingDirectory: commercial-paper/organization/digibank/application + displayName: Install DigiBank Application + - script: | + set -ex + node enrollUser.js + node buy.js + node redeem.js + workingDirectory: commercial-paper/organization/digibank/application + displayName: Digibank Issue Paper diff --git a/ci/templates/commercial-paper/azure-pipelines-java.yml b/ci/templates/commercial-paper/azure-pipelines-java.yml new file mode 100644 index 0000000..6c4ba1b --- /dev/null +++ b/ci/templates/commercial-paper/azure-pipelines-java.yml @@ -0,0 +1,92 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - script: | + ./gradlew build + workingDirectory: commercial-paper/organization/digibank/contract-java + displayName: Build DigiBank Java Contract + - script: | + ./gradlew build + workingDirectory: commercial-paper/organization/magnetocorp/contract-java + displayName: Build MagnetoCorp Java Contract + + - script: | + ./network.sh up createChannel -ca -s couchdb -i ${FABRIC_VERSION} # FABRIC_VERSION is set in ci/azure-pipelines.yml + + # Copy the connection profiles so they are in the correct organizations. + cp "./organizations/peerOrganizations/org1.example.com/connection-org1.yaml" "../commercial-paper/organization/digibank/gateway/" + cp "./organizations/peerOrganizations/org2.example.com/connection-org2.yaml" "../commercial-paper/organization/magnetocorp/gateway/" + workingDirectory: test-network + displayName: Start Fabric + - script: | + source <(./magnetocorp.sh) + peer lifecycle chaincode package cp.tar.gz --lang java --path ./contract-java --label cp_0 + peer lifecycle chaincode install cp.tar.gz + + export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') + echo $PACKAGE_ID + + peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + + peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + workingDirectory: commercial-paper/organization/magnetocorp + displayName: Setup Commercial Paper Contract + - script: | + source <(./digibank.sh) + peer lifecycle chaincode package cp.tar.gz --lang java --path ./contract-java --label cp_0 + peer lifecycle chaincode install cp.tar.gz + + export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') + echo $PACKAGE_ID + + peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + + peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + + peer lifecycle chaincode commit -o localhost:7050 \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel --name papercontract -v 0 \ + --sequence 1 \ + --tls --cafile $ORDERER_CA --waitForEvent + + workingDirectory: commercial-paper/organization/digibank + displayName: Setup Commercial Paper Contract + + - script: retry -- npm install + workingDirectory: commercial-paper/organization/magnetocorp/application + displayName: Install MagnetoCorp Application + - script: | + set -ex + node enrollUser.js + node issue.js + workingDirectory: commercial-paper/organization/magnetocorp/application + displayName: MagnetoCorp Issue Paper + + - script: retry -- npm install + workingDirectory: commercial-paper/organization/digibank/application + displayName: Install DigiBank Application + - script: | + set -ex + node enrollUser.js + node buy.js + node redeem.js + workingDirectory: commercial-paper/organization/digibank/application + displayName: DigiBank Issue Paper diff --git a/ci/templates/commercial-paper/azure-pipelines-javascript.yml b/ci/templates/commercial-paper/azure-pipelines-javascript.yml new file mode 100644 index 0000000..9e30b97 --- /dev/null +++ b/ci/templates/commercial-paper/azure-pipelines-javascript.yml @@ -0,0 +1,82 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - script: | + ./network.sh up createChannel -ca -s couchdb -i ${FABRIC_VERSION} # FABRIC_VERSION is set in ci/azure-pipelines.yml + + # Copy the connection profiles so they are in the correct organizations. + cp "./organizations/peerOrganizations/org1.example.com/connection-org1.yaml" "../commercial-paper/organization/digibank/gateway/" + cp "./organizations/peerOrganizations/org2.example.com/connection-org2.yaml" "../commercial-paper/organization/magnetocorp/gateway/" + workingDirectory: test-network + displayName: Start Fabric + - script: | + source <(./magnetocorp.sh) + peer lifecycle chaincode package cp.tar.gz --lang node --path ./contract --label cp_0 + peer lifecycle chaincode install cp.tar.gz + + export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') + echo $PACKAGE_ID + + peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + + peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + workingDirectory: commercial-paper/organization/magnetocorp + displayName: Setup Commercial Paper Contract + - script: | + source <(./digibank.sh) + peer lifecycle chaincode package cp.tar.gz --lang node --path ./contract --label cp_0 + peer lifecycle chaincode install cp.tar.gz + + export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') + echo $PACKAGE_ID + + peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + + peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + + peer lifecycle chaincode commit -o localhost:7050 \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel --name papercontract -v 0 \ + --sequence 1 \ + --tls --cafile $ORDERER_CA --waitForEvent + + workingDirectory: commercial-paper/organization/digibank + displayName: Setup Commercial Paper Contract + - script: retry -- npm install + workingDirectory: commercial-paper/organization/magnetocorp/application + displayName: Install MagnetoCorp Application + - script: | + set -ex + node enrollUser.js + node issue.js + workingDirectory: commercial-paper/organization/magnetocorp/application + displayName: MagnetoCorp Issue Paper + + - script: retry -- npm install + workingDirectory: commercial-paper/organization/digibank/application + displayName: Install DigiBank Application + - script: | + set -ex + node enrollUser.js + node buy.js + node redeem.js + workingDirectory: commercial-paper/organization/digibank/application + displayName: DigiBank Issue Paper diff --git a/ci/templates/fabcar/azure-pipelines-go.yml b/ci/templates/fabcar/azure-pipelines-go.yml new file mode 100644 index 0000000..6e1562c --- /dev/null +++ b/ci/templates/fabcar/azure-pipelines-go.yml @@ -0,0 +1,21 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - script: ./startFabric.sh go + workingDirectory: fabcar + displayName: Start Fabric + - task: GoTool@0 + displayName: 'Use Go 1.14.2' + inputs: + version: '1.14.2' + - task: Go@0 + displayName: 'go build' + inputs: + command: build + arguments: '-o "fabcar"' + workingDirectory: fabcar/go + - script: DISCOVERY_AS_LOCALHOST=TRUE ./fabcar + workingDirectory: fabcar/go + displayName: Run FabCar Application diff --git a/ci/templates/fabcar/azure-pipelines-java.yml b/ci/templates/fabcar/azure-pipelines-java.yml new file mode 100644 index 0000000..e166f74 --- /dev/null +++ b/ci/templates/fabcar/azure-pipelines-java.yml @@ -0,0 +1,14 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - script: ./startFabric.sh java + workingDirectory: fabcar + displayName: Start Fabric + - script: retry -- mvn dependency:go-offline + workingDirectory: fabcar/java + displayName: Install FabCar Application Dependencies + - script: mvn test + workingDirectory: fabcar/java + displayName: Run FabCar Application diff --git a/ci/templates/fabcar/azure-pipelines-javascript.yml b/ci/templates/fabcar/azure-pipelines-javascript.yml new file mode 100644 index 0000000..dd4f474 --- /dev/null +++ b/ci/templates/fabcar/azure-pipelines-javascript.yml @@ -0,0 +1,21 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - script: ./startFabric.sh javascript + workingDirectory: fabcar + displayName: Start Fabric + - script: | + retry -- npm install + npm ls + workingDirectory: fabcar/javascript + displayName: Install FabCar Application Dependencies + - script: | + set -ex + node enrollAdmin + node registerUser + node invoke + node query + workingDirectory: fabcar/javascript + displayName: Run FabCar Application diff --git a/ci/templates/fabcar/azure-pipelines-typescript.yml b/ci/templates/fabcar/azure-pipelines-typescript.yml new file mode 100644 index 0000000..4e566f5 --- /dev/null +++ b/ci/templates/fabcar/azure-pipelines-typescript.yml @@ -0,0 +1,22 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - script: ./startFabric.sh typescript + workingDirectory: fabcar + displayName: Start Fabric + - script: retry -- npm install + workingDirectory: fabcar/typescript + displayName: Install FabCar Application Dependencies + - script: npm run build + workingDirectory: fabcar/typescript + displayName: Build FabCar application + - script: | + set -ex + node dist/enrollAdmin + node dist/registerUser + node dist/invoke + node dist/query + workingDirectory: fabcar/typescript + displayName: Run FabCar Application diff --git a/ci/templates/install-deps.yml b/ci/templates/install-deps.yml new file mode 100644 index 0000000..9572937 --- /dev/null +++ b/ci/templates/install-deps.yml @@ -0,0 +1,17 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +steps: + - task: NodeTool@0 + inputs: + versionSpec: $(NODE_VER) + displayName: Install Node.js + - script: curl -L --retry 5 --retry-delay 3 https://hyperledger.jfrog.io/hyperledger/fabric-binaries/hyperledger-fabric-linux-amd64-${FABRIC_VERSION}-stable.tar.gz | tar xz + displayName: Download Fabric CLI + - script: curl -L --retry 5 --retry-delay 3 https://hyperledger.jfrog.io/hyperledger/fabric-binaries/hyperledger-fabric-ca-linux-amd64-${FABRIC_VERSION}-stable.tar.gz | tar xz + displayName: Download Fabric CA CLI + - script: curl https://raw.githubusercontent.com/kadwanev/retry/master/retry -o ./bin/retry && chmod +x ./bin/retry + displayName: Install retry CLI + - script: ./ci/scripts/pullFabricImages.sh + displayName: Pull Fabric Docker Imagess \ No newline at end of file diff --git a/commercial-paper/.gitignore b/commercial-paper/.gitignore new file mode 100644 index 0000000..65971ce --- /dev/null +++ b/commercial-paper/.gitignore @@ -0,0 +1,4 @@ +cp.tar.gz +**/.gradle +**/gateway/connection-org1.yaml +**/gateway/connection-org2.yaml diff --git a/commercial-paper/README.md b/commercial-paper/README.md new file mode 100644 index 0000000..779dc6d --- /dev/null +++ b/commercial-paper/README.md @@ -0,0 +1,646 @@ + + +# Commercial Paper Tutorial & Samples + +## Introduction + +This folder contains a structured set of smart contracts and application clients (ie. in a choice of languages, eg Node.js, Java, Go etc) relating to *Commercial Paper*, a finance 'instrument' (in Global Finance). At present, the Node.js sample contract in particular has further added functionality: an optional two-step authority check (see diagram below), when redeeming a commercial paper instance - and a range of sample ledger queries, to help consolidate your learning; both can be explored using the Node.js application client. + +While a more detailed 'explainer' of the Commercial Paper scenario (including use case analysis, code walkthrough & practices, logical/physical representation of ledger data etc) can be found in the [Hyperledger Fabric Commercial Paper Tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/tutorial/commercial_paper.html), you don't have to read through this, just to try out this sample. There's also a [Wikipedia page](https://en.wikipedia.org/wiki/Commercial_paper) + +

Key Objectives + + * see a Commercial Paper use case in action + + * explore the key 'takeaways': understand differences between asset _state_ changes ('e.g. 'lifecycle') and transaction _inputs_* (e.g. 'inputs' during lifecycle) + + * try out a number of different query types: asset history, asset state, ownership, partial key, named query (criteria-based), ad-hoc queries (you supply a query string) - presently available in the Node.js sample only. + + \* the smart contract uses these (along with business logic) to decide outcomes; some inputs change the asset _state_ (like 'ownership') ; some don't. + +

+
+ +
Blockchain: benefits to Commercial Paper marketplaces? + + * replace long-winded, time consuming processing between multiple organisations - the network makes it one centralized hub and helps simplify workflow. + + * full transparency, traceability and ownership of issued papers + + * speed up a process that can take days - e. make same-day issuance a reality, or even a market paradigm. + + * in asset-backed commercial paper markets, blockchain can help increase accessibility (to a marketplace) to SMEs to partake in issuance, where otherwise it was inaccessible. + + * integration to other areas, like supply chain finance + +

+
+ + + Expand the twisty below to see an overview diagram of a 'sample' Commercial paper marketplace - transactions, queries being executed by different organisations (we'll focus on two of these organisations) + +
PaperNet Overview diagram - The sample commercial paper marketplace + +![PaperNet Overview](img/overview.png) + +

+
+ +But first, it might useful to explain Commercial Paper, an unsecured promissory note issued to obtain capital, and operates in global financial markets. A Commercial Paper instance is represented as an asset with a lifecycle, recorded on the blockchain - transactions change its _state_ (over time) and those transactions, naturally - have _inputs_. + + +#### Explainers + +
Commercial Paper - what is it briefly? + +
+It is a type of unsecured promissory note, issued by established companies (eg big manufacturers, blue chip corporations) to gain short-term capital - usually no more than 6-9 months. Why? To meet short-term financial obligations. Commercial paper is generally purchased by money market funds and banks - in fact, it becomes a more important investment strategy during financial recessions :-) . A corporation issues a paper (in the form of a promissory note) for specific projects, such as big capital investments, to pay contractors or even to exercise debt restructuring. The tutorial describes MagnetoCorp (car manufacturer) who have landed a huge contract, and will need approx. $5m in capital (payroll obligations), to hire 1000 car workers for at least 6 months (with no car revenues yet - its a financial strain). Underpinning this, of course, is that MagnetoCorp, has every confidence that (say, in 6 months time) it will be in a position to pay out the face value ($5m in this case) when the commercial paper is redeemd by an owner of the paper, upon maturity :-). +

+
+ +
Ins and Outs, Attractions of Commercial Paper Investment? + +
+Investors (who buy Commercial Paper) are attracted as they agree to buy them at a discount (say $4.94m) on the face value (eg $5m) and moreso, they obtain a higher yield than if they were simply gaining interest in a bank (eg. 2% interest on $4.95m today = $5m in 6 months time). But there is a 'premium' attached, with carrying the risk of a debt/loan that is essentially unsecured (unlike a bank) - which is where credit risk and ratings comes in. As a result, the actual yield from the investment chosen is in effect $10k greater (than pure interest, in the example given). +

+Once an issuing corporation becomes established in the commercial paper marketplace, it builds a high credit rating (risk affects how much of a premium investors seek and therefore discount accordingly) - in fact, it is often cheaper (for a blue chip company) to draw on a commercial paper than on a bank line of credit. But that rating reflects the issuer's ability to pay back on maturity. +

+I mentioned marketplace: even during the typical 6-9 month period, a commercial paper can be bought and sold multiple times (its quoted, at the discounted price on money markets), before the Commercial Paper reaches its maturity date. On that date, the current investor (or owner) 'redeems' the paper bond with MagnetoCorp, the issuer and gets the face value of $5m. + +
+ +[_back to top_](#top) + +## Scenario Overview + +![](https://hyperledger-fabric.readthedocs.io/en/latest/_images/commercial_paper.diagram.1.png) + +In this tutorial two organizations, MagnetoCorp and DigiBank, trade commercial paper with each other on 'PaperNet', the marketplace represented by a Hyperledger Fabric blockchain network. Note that there are two alternative transaction flows - one which mirrors the [Commercial Paper Tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/tutorial/commercial_paper.html) as described in Fabric documentation, and one which requires the authorised owner of the paper to complete a transfer following a request to buy the commercial paper - the latter example features an authorization check in the smart contract that ensures the transactor is from the organization that currently owns the commercial paper - they approve and complete the buy request. These are the commercial paper transaction lifecycles you can try out: + +![Transaction Flow alternatives](img/transaction-flow.png) + +The tutorial exercises the commercial paper asset lifecycle: _issue_, _buy_ ( 1 to _n_ ) (or _buy_request_ / _transfer_ alternative), and _redeem_ transactions: the key 'takeaways' from the scenario are: + +- understanding the _changes in state_ in the commercial paper asset (reflected in the ledger world state) which reaches maturity after 6 months. +- understanding the _transaction inputs_ for each transaction (some inputs change the asset _state_ - eg. ownership) and some _don't_ (e.g. purchase price) and not part of the asset - but importantly, the _inputs_ for a given transaction are recorded on the blockchain). +- understanding the importance of _queries_ such as: asset history, rich queries (criteria matching etc), transaction history (where the inputs are recorded) + +Client applications (CLI based) are used: + +- to perform the transactions +- run queries (Node.js sample only) +- examine the transaction inputs (as opposed to _states_) that are written to the ledger after you perform a transaction (using the Node.js listener). + +This sample uses the `test-network` . You’ll act as Isabella, an employee of MagnetoCorp (Org2), who will issue a commercial paper on its behalf. You’ll then 'switch hats' to take the role of Balaji, an employee of DigiBank (Org1), who will buy this commercial paper, hold it for a period of time, and then redeem it with MagnetoCorp for a small profit or yield. Note that the smart contract sample doesn't enforce the actual hold period ; the user can, in fact, redeem the paper immediately. + + +## Quick Start + +Below are the quick start instructions for running the tutorial. As mentioned, if you're interested in a 'deeper dive' analysis and importance of the concepts, design, structure and implementation of the smart contract, they can be found in the [Hyperledger Fabric Commercial Paper Tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/tutorial/commercial_paper.html). Suffice to say, you DON'T have to have read this, to do this tutorial. + +This `README.md` file is in the `commercial-paper` directory, the source code for client applications and the contracts is in the `organization` directory. + +### High-Level Overview of Steps + +1) Install Binaries, Start the Hyperledger Fabric infrastructure + + The Fabric 'test-network' will be used - this has two organizations 'Org1' and 'Org2' DigiBank will be Org1, and MagnetoCorp will be Org2. + +2) Install and Instantiate the Contracts + +3) Run client applications in the roles of MagnetoCorp and DigiBank to trade the commercial paper + + - Issue the Paper as Magnetocorp (org2) + - Buy the paper as DigiBank (org1) + - Redeem the paper as DigiBank (org1) + + See also the transaction flow and alternatives in the Scenario Overview below. + +[_back to top_](#top) + +## Setup + +You will need a machine with the following + +- Docker and docker-compose installed +- Node.js v12 if you want to run JavaScript client applications +- Java v8 if you want to run Java client applications +- Maven to build the Java applications + +You will need to install the `peer` cli binaries and cloned the `fabric-samples` repository. For more information see +[Install the Samples, Binaries and Docker Images](https://hyperledger-fabric.readthedocs.io/en/latest/install.html) in the Hyperledger Fabric documentation. Once you have installed the cli binaries, ensure you have added the `bin` directory (for your `peer` commands used by scripts below) to your exported `PATH` variable in your `.bashrc` or `.profile` directory (per below). This is important as you will be opening a number of windows which will need PATH set. Finally, check that it finds the `peer` command in your PATH using the `which` command eg. + +``` +export PATH=:$PATH +which peer +``` + + +It is advised to have 3 terminal windows (consoles) open; + +* one to monitor the infrastructure +* one for MagnetoCorp +* one for DigiBank. + +Once you've cloned the `fabric-samples` - change to the `commercial-paper` directory in each window. + +``` +git clone https://github.com/hyperledger/fabric-samples.git +``` + +``` +cd fabric-samples/commercial-paper +``` + +## Running the Infrastructure + +In one console window, run the network starter script - this will start the two organization `test-network` blockchain network. + +``` +./network-starter.sh +``` + +You can re-use this console window if you wish, but it is recommended to run a docker container monitoring script in its own window. This will let you view what Fabric is doing and help diagnose any failures. + +```bash +./organization/magnetocorp/configuration/cli/monitordocker.sh net_test +``` + +### Setup the Organizations' environments + +The contract code is available as either JavaScript, Java or Go. You can use either one, and the choice of contract language does not affect the choice of client language. With the v2.0 Fabric chaincode lifecycle, this requires operations for both MagnetoCorp and Digibank admin. Open two windows in the fabric-samples/commercial paper directory, one for each organization. + +In your 'MagnetoCorp' window run the following commands, to set the shell environment variables needed to act as that organization. The leading '.' in the command sequence sets in your current environment - if you do not run this, you will not be able to package the chaincode. + +``` +cd fabric-samples/commercial-paper/organization/magnetocorp +. ./magnetocorp.sh +``` + +You can either copy and paste them directly into the terminal, or invoke directly in your own command shell. For example if you are using bash or zsh on Linux you can use this command. + +``` +source <(./magnetocorp.sh) +``` + +Similarly in your 'DigiBank' window run the following commands as shown: + +``` +cd fabric-samples/commercial-paper/organization/digibank +. ./digibank.sh +``` + +[_back to top_](#top) + + +### Deploy the smart contract to the channel + +You need to perform similar operations for _both_ organizations and for your language choice from the instructions below. For the different contract languages, the steps are very similar - the full set of steps are actually shown in the JavaScript section (see twisty). However, you will perform one or two different initial steps for Java or Go before completing the remaining common steps as instructed in those language sections. + +Note that the commands below make use of the `jq` utility for parsing output - download and install it from [the jq download page](https://stedolan.github.io/jq/download/). + + +**
For a JavaScript Contract** + + +Running in MagnetoCorp directory: + +``` +# MAGNETOCORP + +peer lifecycle chaincode package cp.tar.gz --lang node --path ./contract --label cp_0 +peer lifecycle chaincode install cp.tar.gz + +export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') +echo $PACKAGE_ID # FYI may look like this: 'cp_0:nnnxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' +``` + +``` + +peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + +peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 +``` + +Running in Digibank directory: + +``` + +# DIGIBANK + +peer lifecycle chaincode package cp.tar.gz --lang node --path ./contract --label cp_0 +peer lifecycle chaincode install cp.tar.gz + +export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') +echo $PACKAGE_ID + +peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + +peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + +``` + +Once both organizations have installed, and approved the chaincode, it can be committed. + +``` +# MAGNETOCORP + +peer lifecycle chaincode commit -o localhost:7050 \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel --name papercontract -v 0 \ + --sequence 1 \ + --tls --cafile $ORDERER_CA --waitForEvent + +``` + +To test, try sending some simple transactions. + +``` + +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --channelID mychannel --name papercontract \ + -c '{"Args":["org.papernet.commercialpaper:instantiate"]}' ${PEER_ADDRESS_ORG1} ${PEER_ADDRESS_ORG2} \ + --tls --cafile $ORDERER_CA --waitForEvent + +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --tls --cafile $ORDERER_CA | jq '.' -C | more +``` +

+
+ + +**
For a Java Contract:** + + +Before the `peer lifecycle chaincode package` command below, you will first need to change into each organization's `contract-java` directory and issue + +``` +./gradlew build +``` + +Then complete the steps below. + + +Running in MagnetoCorp contract directory: + +``` +# MAGNETOCORP + +peer lifecycle chaincode package cp.tar.gz --lang java --path ./contract-java --label cp_0 +peer lifecycle chaincode install cp.tar.gz + +export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') +echo $PACKAGE_ID # FYI may look like this: 'cp_0:nnnxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' +``` + +``` + +peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + +peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 +``` + +Running in Digibank + +``` + +# DIGIBANK + +peer lifecycle chaincode package cp.tar.gz --lang java --path ./contract-java --label cp_0 +peer lifecycle chaincode install cp.tar.gz + +export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') +echo $PACKAGE_ID + +peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + +peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + +``` + +Once both organizations have installed, and approved the chaincode, it can be committed. + +``` +# MAGNETOCORP + +peer lifecycle chaincode commit -o localhost:7050 \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel --name papercontract -v 0 \ + --sequence 1 \ + --tls --cafile $ORDERER_CA --waitForEvent + +``` + +To test, try sending some simple transactions. + +``` + +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --channelID mychannel --name papercontract \ + -c '{"Args":["org.papernet.commercialpaper:instantiate"]}' ${PEER_ADDRESS_ORG1} ${PEER_ADDRESS_ORG2} \ + --tls --cafile $ORDERER_CA --waitForEvent + +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --tls --cafile $ORDERER_CA | jq '.' -C | more +``` + +

+
+ +**
For a Go Contract** + + +Before the `peer lifecycle chaincode package` command step, you will need to change into each organization's `contract-go` directory and issue + +``` +go mod vendor +``` + +Then complete the steps below. + + +Running in MagnetoCorp contract directory: + +``` +# MAGNETOCORP + +peer lifecycle chaincode package cp.tar.gz --lang golang --path ./contract-go --label cp_0 +peer lifecycle chaincode install cp.tar.gz + +export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') +echo $PACKAGE_ID # FYI may look like this: 'cp_0:nnnxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' +``` + +``` + +peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + +peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 +``` + +Running in Digibank + +``` + +# DIGIBANK + +peer lifecycle chaincode package cp.tar.gz --lang golang --path ./contract-go --label cp_0 +peer lifecycle chaincode install cp.tar.gz + +export PACKAGE_ID=$(peer lifecycle chaincode queryinstalled --output json | jq -r '.installed_chaincodes[0].package_id') +echo $PACKAGE_ID + +peer lifecycle chaincode approveformyorg --orderer localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -v 0 \ + --package-id $PACKAGE_ID \ + --sequence 1 \ + --tls \ + --cafile $ORDERER_CA + +peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name papercontract -v 0 --sequence 1 + +``` + +Once both organizations have installed, and approved the chaincode, it can be committed. + +``` +# MAGNETOCORP + +peer lifecycle chaincode commit -o localhost:7050 \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel --name papercontract -v 0 \ + --sequence 1 \ + --tls --cafile $ORDERER_CA --waitForEvent + +``` + +To test, try sending some simple transactions. + +``` + +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --peerAddresses localhost:7051 --tlsRootCertFiles ${PEER0_ORG1_CA} \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --channelID mychannel --name papercontract \ + -c '{"Args":["org.papernet.commercialpaper:instantiate"]}' ${PEER_ADDRESS_ORG1} ${PEER_ADDRESS_ORG2} \ + --tls --cafile $ORDERER_CA --waitForEvent + +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com \ + --channelID mychannel \ + --name papercontract \ + -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' \ + --peerAddresses localhost:9051 --tlsRootCertFiles ${PEER0_ORG2_CA} \ + --tls --cafile $ORDERER_CA | jq '.' -C | more +``` + + + +

+
+ + +[_back to top_](#top) + + +## Client Applications + +Note for Java applications, you will need to compile the Java Code using `maven`. Use this command in each application-java directory + +``` +mvn clean package +``` + +Note for JavaScript applications, you will need to install the dependencies first. Use this command in each application directory + +``` +npm install +``` +> Note that there is NO dependency between the language of any one client application and any contract. Mix and match as you wish! + +The docker containers don't contain the node or Java runtimes; so it is best to exit the docker containers - but keep the windows open and run the applications locally. + +As mentioned earlier in the Sample introduction section, transaction _inputs_ are recorded on the ledger, as well as any asset _state_ changes. Just *before* you run the _issue_ application script below - you need to launch a block 'listener' application that will show you these _inputs_, as you complete each transaction in the commercial paper lifecycle (eg. Paper Number: 00001, 00002 etc) . + +For the listener, its best to open a *new* terminal for this in the `commercial-paper/organization/magnetocorp/application` directory (javascript). Next, run the `addToWallet` step in the `issue` transaction below, to add Isabella's identity to the wallet - the listener will use this wallet. Once the listener is launched, it will show the inputs for transactions you will perform and which are committed to blocks (ie part of the ledger). Note: initially, the listener may show a spurious message, and then go into a _listening_ or 'wait' state. As transactions complete below, messages will be displayed by the listener - so keep an eye out. *After* adding Isabella's wallet, you can then launch the listener as follows: + +``` +node cpListener.js +``` + +**
Issue the commercial paper** + +The paper is issued by *MagnetoCorp* + +You can now run the applications to issue the commercial paper. Change to either the +`commercial-paper/organization/magnetocorp/application` directory (javascript) or `commercial-paper/organization/magnetocorp/application-java` directory (java) + +*Add the Identity to be used to the wallet* + +``` +node addToWallet.js +# or +java -cp target/commercial-paper-0.0.1-SNAPSHOT.jar org.magnetocorp.AddToWallet +``` + +*Issue the Commercial Paper* + +``` +node issue.js +# or +java -cp target/commercial-paper-0.0.1-SNAPSHOT.jar org.magnetocorp.Issue +``` + +Don't forget to check the application listener for messages above! + +

+
+ + +**
Buy the commercial paper** + +_Buy_ is performed as *Digibank*; + +You can now run the applications to buy the paper. Change to either the +`commercial-paper/organization/digibank/application` directory or `commercial-paper/organization/digibank/application-java` + +*Add the Identity to be used* + +``` +node addToWallet.js +# or +java -cp target/commercial-paper-0.0.1-SNAPSHOT.jar org.digibank.AddToWallet +``` + +*Buy the paper* + +``` +node buy.js +# or +java -cp target/commercial-paper-0.0.1-SNAPSHOT.jar org.digibank.Buy +``` + +If you have just executed a `buy` transaction above - jump to the `redeem` transaction below - otherwise execute the _buy_/_transfer_ sequence as described earlier. + +*Alternative: Request to Buy the paper (buy/transfer)* + +``` +node buy_request.js +``` + +Now complete the _request_ by switching to the `MagnetoCorp` application directory (javascript) and execute a `transfer` transaction as MagnetoCorp: + +``` +cd ../../magnetocorp/application + +node transfer.js +``` + +

+
+ +**
Redeem the commercial paper** + +_Redeem_ is performed as *Digibank* - the current owner (buyer) in the lifecycle. + +You can now run the applications to redeem the paper. Change to either the +`commercial-paper/organization/digibank/application` directory or `commercial-paper/organization/digibank/application-java` + + +*Redeem* + +``` +node redeem.js +# or +java -cp target/commercial-paper-0.0.1-SNAPSHOT.jar org.digibank.Redeem +``` + +

+
+ + +**
Perform Queries: Ownership, Asset History etc (Node.js sample only) ** + + Having completed the full commercial paper lifecycle for one paper (paper number: 00001) some queries below won't show a lot of data - as an optional exercise, you can change the scripts above (paper number: 00002) to create another paper lifecycle and run the `queryapp` application below (change query 1 to the new CP number FYI), with more data available. As indicated, the query transactions mentioned are presently only available in the Node.js sample. + + Execute the Node.js application client script, which will run the following 5 queries, in order: + + - History of Commercial Paper (Note: the paper state is shown more descriptively eg. 'ISSUED', 'TRADING' and based on currentState values on ledger) + - Ownership of Commercial Papers + - Partial Key query, for Commercial papers in org.papernet.papers namespace belonging to MagnetoCorp + - Named Query: all redeemed papers in a state of 'redeemed' (currentState = 4) + - Named Query: all commercial papers with a face value > $4m + + From the `digibank/application` subdirectory run: + + ``` + node queryapp.js + ``` + +

+
+ +When you're done with this section, return to the terminal where your Node.js _listener_ application is running, and terminate the process. + +## Clean up +When you are finished using the Fabric test network and the commercial paper smart contract and applications, you can use the following command to clean up the network: + +``` +./network-clean.sh +``` + +[_back to top_](#top) diff --git a/commercial-paper/img/overview.png b/commercial-paper/img/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..9da7aced3a6f91968535c95aa52433bff87881cd GIT binary patch literal 122143 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%0AM-pdkFTW=ixLJa?v{5dpI0M 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≪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 literal 0 HcmV?d00001 diff --git a/commercial-paper/network-clean.sh b/commercial-paper/network-clean.sh new file mode 100755 index 0000000..5fe6c08 --- /dev/null +++ b/commercial-paper/network-clean.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# SPDX-License-Identifier: Apache-2.0 + +function _exit(){ + printf "Exiting:%s\n" "$1" + exit -1 +} + +# Exit on first error, print all commands. +set -ev +set -o pipefail + +# Where am I? +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +export FABRIC_CFG_PATH="${DIR}/../config" + +cd "${DIR}/../test-network/" + +docker kill cliDigiBank cliMagnetoCorp logspout || true +./network.sh down + +# remove any stopped containers +docker rm $(docker ps -aq) + diff --git a/commercial-paper/network-starter.sh b/commercial-paper/network-starter.sh new file mode 100755 index 0000000..c2fe682 --- /dev/null +++ b/commercial-paper/network-starter.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# SPDX-License-Identifier: Apache-2.0 + +function _exit(){ + printf "Exiting:%s\n" "$1" + exit -1 +} + +# Exit on first error, print all commands. +set -ev +set -o pipefail + +# Where am I? +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +export FABRIC_CFG_PATH="${DIR}/../config" + +cd "${DIR}/../test-network/" + +docker kill cliDigiBank cliMagnetoCorp logspout || true +./network.sh down +./network.sh up createChannel -ca -s couchdb + +# Copy the connection profiles so they are in the correct organizations. +cp "${DIR}/../test-network/organizations/peerOrganizations/org1.example.com/connection-org1.yaml" "${DIR}/organization/digibank/gateway/" +cp "${DIR}/../test-network/organizations/peerOrganizations/org2.example.com/connection-org2.yaml" "${DIR}/organization/magnetocorp/gateway/" + +cp ${DIR}/../test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/* ${DIR}/../test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem +cp ${DIR}/../test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/* ${DIR}/../test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk + +cp ${DIR}/../test-network/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/signcerts/* ${DIR}/../test-network/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/signcerts/User1@org2.example.com-cert.pem +cp ${DIR}/../test-network/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/keystore/* ${DIR}/../test-network/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/keystore/priv_sk + +echo Suggest that you monitor the docker containers by running +echo "./organization/magnetocorp/configuration/cli/monitordocker.sh net_test" diff --git a/commercial-paper/organization/digibank/.gitignore b/commercial-paper/organization/digibank/.gitignore new file mode 100644 index 0000000..7d13901 --- /dev/null +++ b/commercial-paper/organization/digibank/.gitignore @@ -0,0 +1 @@ +identity \ No newline at end of file diff --git a/commercial-paper/organization/digibank/application-java/.gitignore b/commercial-paper/organization/digibank/application-java/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/commercial-paper/organization/digibank/application-java/.settings/org.eclipse.jdt.core.prefs b/commercial-paper/organization/digibank/application-java/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b8947ec --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/commercial-paper/organization/digibank/application-java/.settings/org.eclipse.m2e.core.prefs b/commercial-paper/organization/digibank/application-java/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/commercial-paper/organization/digibank/application-java/pom.xml b/commercial-paper/organization/digibank/application-java/pom.xml new file mode 100644 index 0000000..f80dc68 --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/pom.xml @@ -0,0 +1,96 @@ + + 4.0.0 + commercial-paper + commercial-paper + 0.0.1-SNAPSHOT + + + + + 1.8 + UTF-8 + UTF-8 + + + [2.0,3.0) + + 2.1.0 + + + + + src + + + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.0 + + + + package + + shade + + + + + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + jitpack.io + https://jitpack.io + + + + + + org.hyperledger.fabric + fabric-gateway-java + ${fabric-gateway-java.version} + + + + + org.hyperledger.fabric-chaincode-java + fabric-chaincode-shim + ${fabric-chaincode-java.version} + compile + + + + + org.json + json + 20180813 + + + + diff --git a/commercial-paper/organization/digibank/application-java/src/org/digibank/AddToWallet.java b/commercial-paper/organization/digibank/application-java/src/org/digibank/AddToWallet.java new file mode 100644 index 0000000..6a3fa12 --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/src/org/digibank/AddToWallet.java @@ -0,0 +1,68 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.digibank; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Identity; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; + +public class AddToWallet { + + private static X509Certificate readX509Certificate(final Path certificatePath) throws IOException, CertificateException { + try (Reader certificateReader = Files.newBufferedReader(certificatePath, StandardCharsets.UTF_8)) { + return Identities.readX509Certificate(certificateReader); + } + } + + private static PrivateKey getPrivateKey(final Path privateKeyPath) throws IOException, InvalidKeyException { + try (Reader privateKeyReader = Files.newBufferedReader(privateKeyPath, StandardCharsets.UTF_8)) { + return Identities.readPrivateKey(privateKeyReader); + } + } + + public static void main(String[] args) { + try { + // A wallet stores a collection of identities + Path walletPath = Paths.get(".", "wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + + Path credentialPath = Paths.get("..", "..", "..",".." ,"test-network", "organizations", + "peerOrganizations", "org1.example.com", "users", "User1@org1.example.com", "msp"); + System.out.println("credentialPath: " + credentialPath.toString()); + Path certificatePath = credentialPath.resolve(Paths.get("signcerts", + "User1@org1.example.com-cert.pem")); + System.out.println("certificatePem: " + certificatePath.toString()); + Path privateKeyPath = credentialPath.resolve(Paths.get("keystore", + "priv_sk")); + + X509Certificate certificate = readX509Certificate(certificatePath); + PrivateKey privateKey = getPrivateKey(privateKeyPath); + + Identity identity = Identities.newX509Identity("Org1MSP", certificate, privateKey); + + + String identityLabel = "User1@org1.example.com"; + wallet.put(identityLabel, identity); + + System.out.println("Write wallet info into " + walletPath.toString() + " successfully."); + + } catch (IOException | CertificateException | InvalidKeyException e) { + System.err.println("Error adding to wallet"); + e.printStackTrace(); + } + } + +} diff --git a/commercial-paper/organization/digibank/application-java/src/org/digibank/Buy.java b/commercial-paper/organization/digibank/application-java/src/org/digibank/Buy.java new file mode 100644 index 0000000..5e09bbd --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/src/org/digibank/Buy.java @@ -0,0 +1,74 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.digibank; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +import org.hyperledger.fabric.gateway.Contract; +import org.hyperledger.fabric.gateway.Gateway; +import org.hyperledger.fabric.gateway.GatewayException; +import org.hyperledger.fabric.gateway.Network; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.papernet.CommercialPaper; + +public class Buy { + + private static final String ENVKEY="CONTRACT_NAME"; + + public static void main(String[] args) { + Gateway.Builder builder = Gateway.createBuilder(); + + String contractName="papercontract"; + // get the name of the contract, in case it is overridden + Map envvar = System.getenv(); + if (envvar.containsKey(ENVKEY)){ + contractName=envvar.get(ENVKEY); + } + + try { + // A wallet stores a collection of identities + Path walletPath = Paths.get(".", "wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + System.out.println("Read wallet info from: " + walletPath); + + String userName = "User1@org1.example.com"; + + Path connectionProfile = Paths.get("..", "gateway", "connection-org1.yaml"); + + // Set connection options on the gateway builder + builder.identity(wallet, userName).networkConfig(connectionProfile).discovery(false); + + // Connect to gateway using application specified parameters + try(Gateway gateway = builder.connect()) { + + // Access PaperNet network + System.out.println("Use network channel: mychannel."); + Network network = gateway.getNetwork("mychannel"); + + // Get addressability to commercial paper contract + System.out.println("Use org.papernet.commercialpaper smart contract."); + Contract contract = network.getContract(contractName, "org.papernet.commercialpaper"); + + // Buy commercial paper + System.out.println("Submit commercial paper buy transaction."); + byte[] response = contract.submitTransaction("buy", "MagnetoCorp", "00001", "MagnetoCorp", "DigiBank", "4900000", "2020-05-31"); + + // Process response + System.out.println("Process buy transaction response."); + CommercialPaper paper = CommercialPaper.deserialize(response); + System.out.println(paper); + } + } catch (GatewayException | IOException | TimeoutException | InterruptedException e) { + e.printStackTrace(); + System.exit(-1); + } + } + +} diff --git a/commercial-paper/organization/digibank/application-java/src/org/digibank/Redeem.java b/commercial-paper/organization/digibank/application-java/src/org/digibank/Redeem.java new file mode 100644 index 0000000..62eb9d9 --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/src/org/digibank/Redeem.java @@ -0,0 +1,73 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.digibank; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +import org.hyperledger.fabric.gateway.Contract; +import org.hyperledger.fabric.gateway.Gateway; +import org.hyperledger.fabric.gateway.GatewayException; +import org.hyperledger.fabric.gateway.Network; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.papernet.CommercialPaper; + +public class Redeem { + + private static final String ENVKEY="CONTRACT_NAME"; + + public static void main(String[] args) { + Gateway.Builder builder = Gateway.createBuilder(); + + String contractName="papercontract"; + // get the name of the contract, in case it is overridden + Map envvar = System.getenv(); + if (envvar.containsKey(ENVKEY)){ + contractName=envvar.get(ENVKEY); + } + + try { + // A wallet stores a collection of identities + Path walletPath = Paths.get(".", "wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + + String userName = "User1@org1.example.com"; + + Path connectionProfile = Paths.get("..", "gateway", "connection-org1.yaml"); + + // Set connection options on the gateway builder + builder.identity(wallet, userName).networkConfig(connectionProfile).discovery(false); + + // Connect to gateway using application specified parameters + try(Gateway gateway = builder.connect()) { + + // Access PaperNet network + System.out.println("Use network channel: mychannel."); + Network network = gateway.getNetwork("mychannel"); + + // Get addressability to commercial paper contract + System.out.println("Use org.papernet.commercialpaper smart contract."); + Contract contract = network.getContract("papercontract", "org.papernet.commercialpaper"); + + // Redeem commercial paper + System.out.println("Submit commercial paper redeem transaction."); + byte[] response = contract.submitTransaction("redeem", "MagnetoCorp", "00001", "DigiBank", "2020-11-30"); + + // Process response + System.out.println("Process redeem transaction response."); + CommercialPaper paper = CommercialPaper.deserialize(response); + System.out.println(paper); + } + } catch (GatewayException | IOException | TimeoutException | InterruptedException e) { + e.printStackTrace(); + System.exit(-1); + } + } + +} diff --git a/commercial-paper/organization/digibank/application-java/src/org/papernet/CommercialPaper.java b/commercial-paper/organization/digibank/application-java/src/org/papernet/CommercialPaper.java new file mode 100644 index 0000000..dbb4e3f --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/src/org/papernet/CommercialPaper.java @@ -0,0 +1,181 @@ +/* + * SPDX-License-Identifier: + */ + +package org.papernet; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.papernet.ledgerapi.State; +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; +import org.json.JSONObject; +import org.json.JSONPropertyIgnore; + +@DataType() +public class CommercialPaper extends State { + // Enumerate commercial paper state values + public final static String ISSUED = "ISSUED"; + public final static String TRADING = "TRADING"; + public final static String REDEEMED = "REDEEMED"; + + @Property() + private String state=""; + + public String getState() { + return state; + } + + public CommercialPaper setState(String state) { + this.state = state; + return this; + } + + @JSONPropertyIgnore() + public boolean isIssued() { + return this.state.equals(CommercialPaper.ISSUED); + } + + @JSONPropertyIgnore() + public boolean isTrading() { + return this.state.equals(CommercialPaper.TRADING); + } + + @JSONPropertyIgnore() + public boolean isRedeemed() { + return this.state.equals(CommercialPaper.REDEEMED); + } + + public CommercialPaper setIssued() { + this.state = CommercialPaper.ISSUED; + return this; + } + + public CommercialPaper setTrading() { + this.state = CommercialPaper.TRADING; + return this; + } + + public CommercialPaper setRedeemed() { + this.state = CommercialPaper.REDEEMED; + return this; + } + + @Property() + private String paperNumber; + + @Property() + private String issuer; + + @Property() + private String issueDateTime; + + @Property() + private int faceValue; + + @Property() + private String maturityDateTime; + + @Property() + private String owner; + + public String getOwner() { + return owner; + } + + public CommercialPaper setOwner(String owner) { + this.owner = owner; + return this; + } + + public CommercialPaper() { + super(); + } + + public CommercialPaper setKey() { + this.key = State.makeKey(new String[] { this.paperNumber }); + return this; + } + + public String getPaperNumber() { + return paperNumber; + } + + public CommercialPaper setPaperNumber(String paperNumber) { + this.paperNumber = paperNumber; + return this; + } + + public String getIssuer() { + return issuer; + } + + public CommercialPaper setIssuer(String issuer) { + this.issuer = issuer; + return this; + } + + public String getIssueDateTime() { + return issueDateTime; + } + + public CommercialPaper setIssueDateTime(String issueDateTime) { + this.issueDateTime = issueDateTime; + return this; + } + + public int getFaceValue() { + return faceValue; + } + + public CommercialPaper setFaceValue(int faceValue) { + this.faceValue = faceValue; + return this; + } + + public String getMaturityDateTime() { + return maturityDateTime; + } + + public CommercialPaper setMaturityDateTime(String maturityDateTime) { + this.maturityDateTime = maturityDateTime; + return this; + } + + @Override + public String toString() { + return "Paper::" + this.key + " " + this.getPaperNumber() + " " + getIssuer() + " " + getFaceValue(); + } + + /** + * Deserialize a state data to commercial paper + * + * @param {Buffer} data to form back into the object + */ + public static CommercialPaper deserialize(byte[] data) { + JSONObject json = new JSONObject(new String(data, UTF_8)); + + String issuer = json.getString("issuer"); + String paperNumber = json.getString("paperNumber"); + String issueDateTime = json.getString("issueDateTime"); + String maturityDateTime = json.getString("maturityDateTime"); + String owner = json.getString("owner"); + int faceValue = json.getInt("faceValue"); + String state = json.getString("state"); + return createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue,owner,state); + } + + public static byte[] serialize(CommercialPaper paper) { + return State.serialize(paper); + } + + /** + * Factory method to create a commercial paper object + */ + public static CommercialPaper createInstance(String issuer, String paperNumber, String issueDateTime, + String maturityDateTime, int faceValue, String owner, String state) { + return new CommercialPaper().setIssuer(issuer).setPaperNumber(paperNumber).setMaturityDateTime(maturityDateTime) + .setFaceValue(faceValue).setKey().setIssueDateTime(issueDateTime).setOwner(owner).setState(state); + } + +} diff --git a/commercial-paper/organization/digibank/application-java/src/org/papernet/ledgerapi/State.java b/commercial-paper/organization/digibank/application-java/src/org/papernet/ledgerapi/State.java new file mode 100644 index 0000000..a32abc0 --- /dev/null +++ b/commercial-paper/organization/digibank/application-java/src/org/papernet/ledgerapi/State.java @@ -0,0 +1,60 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ +package org.papernet.ledgerapi; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.json.JSONObject; + +/** + * State class. States have a class, unique key, and a lifecycle current state + * the current state is determined by the specific subclass + */ +public class State { + + protected String key; + + /** + * @param {String|Object} class An identifiable class of the instance + * @param {keyParts[]} elements to pull together to make a key for the objects + */ + public State() { + + } + + String getKey() { + return this.key; + } + + public String[] getSplitKey() { + return State.splitKey(this.key); + } + + /** + * Convert object to buffer containing JSON data serialization Typically used + * before putState()ledger API + * + * @param {Object} JSON object to serialize + * @return {buffer} buffer with the data to store + */ + public static byte[] serialize(Object object) { + String jsonStr = new JSONObject(object).toString(); + return jsonStr.getBytes(UTF_8); + } + + /** + * Join the keyParts to make a unififed string + * + * @param (String[]) keyParts + */ + public static String makeKey(String[] keyParts) { + return String.join(":", keyParts); + } + + public static String[] splitKey(String key) { + System.out.println("splitting key " + key + " " + java.util.Arrays.asList(key.split(":"))); + return key.split(":"); + } + +} diff --git a/commercial-paper/organization/digibank/application/.eslintrc.js b/commercial-paper/organization/digibank/application/.eslintrc.js new file mode 100644 index 0000000..22fbefc --- /dev/null +++ b/commercial-paper/organization/digibank/application/.eslintrc.js @@ -0,0 +1,37 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +module.exports = { + env: { + node: 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 newline at end of file diff --git a/commercial-paper/organization/digibank/application/.gitignore b/commercial-paper/organization/digibank/application/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/commercial-paper/organization/digibank/application/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/commercial-paper/organization/digibank/application/addToWallet.js b/commercial-paper/organization/digibank/application/addToWallet.js new file mode 100644 index 0000000..9517e39 --- /dev/null +++ b/commercial-paper/organization/digibank/application/addToWallet.js @@ -0,0 +1,55 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const { Wallets } = require('fabric-network'); +const path = require('path'); + +const fixtures = path.resolve(__dirname, '../../../../test-network'); + +async function main() { + + // Main try/catch block + try { + + // A wallet stores a collection of identities + const wallet = await Wallets.newFileSystemWallet('../identity/user/balaji/wallet'); + + // Identity to credentials to be stored in the wallet + const credPath = path.join(fixtures, '/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com'); + const certificate = fs.readFileSync(path.join(credPath, '/msp/signcerts/User1@org1.example.com-cert.pem')).toString(); + const privateKey = fs.readFileSync(path.join(credPath, '/msp/keystore/priv_sk')).toString(); + + // Load credentials into wallet + const identityLabel = 'balaji'; + + const identity = { + credentials: { + certificate, + privateKey + }, + mspId: 'Org1MSP', + type: 'X.509' + } + + await wallet.put(identityLabel, identity); + + } catch (error) { + console.log(`Error adding to wallet. ${error}`); + console.log(error.stack); + } +} + +main().then(() => { + console.log('done'); +}).catch((e) => { + console.log(e); + console.log(e.stack); + process.exit(-1); +}); diff --git a/commercial-paper/organization/digibank/application/buy.js b/commercial-paper/organization/digibank/application/buy.js new file mode 100644 index 0000000..b052b75 --- /dev/null +++ b/commercial-paper/organization/digibank/application/buy.js @@ -0,0 +1,105 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +/* + * This application has 6 basic steps: + * 1. Select an identity from a wallet + * 2. Connect to network gateway + * 3. Access PaperNet network + * 4. Construct request to buy commercial paper + * 5. Submit transaction + * 6. Process response + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const yaml = require('js-yaml'); +const { Wallets, Gateway } = require('fabric-network'); +const CommercialPaper = require('../../magnetocorp/contract/lib/paper.js'); + + +// Main program function +async function main () { + + // A wallet stores a collection of identities for use + const wallet = await Wallets.newFileSystemWallet('../identity/user/balaji/wallet'); + + + // A gateway defines the peers used to access Fabric networks + const gateway = new Gateway(); + + // Main try/catch block + try { + + // Specify userName for network access + const userName = 'balaji'; + + // Load connection profile; will be used to locate a gateway + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org1.yaml', 'utf8')); + + // Set connection options; identity and wallet + let connectionOptions = { + identity: userName, + wallet: wallet, + discovery: { enabled: true, asLocalhost: true } + + }; + + // Connect to gateway using application specified parameters + console.log('Connect to Fabric gateway.'); + + await gateway.connect(connectionProfile, connectionOptions); + + // Access PaperNet network + console.log('Use network channel: mychannel.'); + + const network = await gateway.getNetwork('mychannel'); + + // Get addressability to commercial paper contract + console.log('Use org.papernet.commercialpaper smart contract.'); + + const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper'); + + // buy commercial paper + console.log('Submit commercial paper buy transaction.'); + + const buyResponse = await contract.submitTransaction('buy', 'MagnetoCorp', '00001', 'MagnetoCorp', 'DigiBank', '4900000', '2020-05-31'); + + // process response + console.log('Process buy transaction response.'); + + let paper = CommercialPaper.fromBuffer(buyResponse); + + console.log(`${paper.issuer} commercial paper : ${paper.paperNumber} successfully purchased by ${paper.owner}`); + console.log('Transaction complete.'); + + } catch (error) { + + console.log(`Error processing transaction. ${error}`); + console.log(error.stack); + + } finally { + + // Disconnect from the gateway + console.log('Disconnect from Fabric gateway.'); + gateway.disconnect(); + + } +} +main().then(() => { + + console.log('Buy program complete.'); + +}).catch((e) => { + + console.log('Buy program exception.'); + console.log(e); + console.log(e.stack); + process.exit(-1); + +}); diff --git a/commercial-paper/organization/digibank/application/buy_request.js b/commercial-paper/organization/digibank/application/buy_request.js new file mode 100644 index 0000000..1493b80 --- /dev/null +++ b/commercial-paper/organization/digibank/application/buy_request.js @@ -0,0 +1,105 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +/* + * This application has 6 basic steps: + * 1. Select an identity from a wallet + * 2. Connect to network gateway + * 3. Access PaperNet network + * 4. Construct request to buy (buy_request) commercial paper + * 5. Submit transaction + * 6. Process response + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const yaml = require('js-yaml'); +const { Wallets, Gateway } = require('fabric-network'); +const CommercialPaper = require('../../magnetocorp/contract/lib/paper.js'); + + +// Main program function +async function main () { + + // A wallet stores a collection of identities for use + const wallet = await Wallets.newFileSystemWallet('../identity/user/balaji/wallet'); + + + // A gateway defines the peers used to access Fabric networks + const gateway = new Gateway(); + + // Main try/catch block + try { + + // Specify userName for network access + const userName = 'balaji'; + + // Load connection profile; will be used to locate a gateway + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org1.yaml', 'utf8')); + + // Set connection options; identity and wallet + let connectionOptions = { + identity: userName, + wallet: wallet, + discovery: { enabled: true, asLocalhost: true } + + }; + + // Connect to gateway using application specified parameters + console.log('Connect to Fabric gateway.'); + + await gateway.connect(connectionProfile, connectionOptions); + + // Access PaperNet network + console.log('Use network channel: mychannel.'); + + const network = await gateway.getNetwork('mychannel'); + + // Get addressability to commercial paper contract + console.log('Use org.papernet.commercialpaper smart contract.'); + + const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper'); + + // request to buy commercial paper using buy_request / transfer two-part transaction + console.log('Submit commercial paper buy_request transaction.'); + + const buyResponse = await contract.submitTransaction('buy_request', 'MagnetoCorp', '00001', 'MagnetoCorp', 'DigiBank', '4900000', '2020-05-31'); + + // process response + console.log('Process buy_request transaction response.'); + + let paper = CommercialPaper.fromBuffer(buyResponse); + + console.log(`${paper.issuer} commercial paper : ${paper.paperNumber} has been provisionally purchased : the transfer must now be completed by paper owner`); + console.log('Transaction complete.'); + + } catch (error) { + + console.log(`Error processing transaction. ${error}`); + console.log(error.stack); + + } finally { + + // Disconnect from the gateway + console.log('Disconnect from Fabric gateway.'); + gateway.disconnect(); + + } +} +main().then(() => { + + console.log('Buy_request program complete.'); + +}).catch((e) => { + + console.log('Buy_request program exception.'); + console.log(e); + console.log(e.stack); + process.exit(-1); + +}); diff --git a/commercial-paper/organization/digibank/application/enrollUser.js b/commercial-paper/organization/digibank/application/enrollUser.js new file mode 100644 index 0000000..2df5d6a --- /dev/null +++ b/commercial-paper/organization/digibank/application/enrollUser.js @@ -0,0 +1,56 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const FabricCAServices = require('fabric-ca-client'); +const { Wallets } = require('fabric-network'); +const fs = require('fs'); +const yaml = require('js-yaml'); +const path = require('path'); + +async function main() { + try { + // load the network configuration + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org1.yaml', 'utf8')); + + // Create a new CA client for interacting with the CA. + const caInfo = connectionProfile.certificateAuthorities['ca.org1.example.com']; + const caTLSCACerts = caInfo.tlsCACerts.pem; + const ca = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), '../identity/user/balaji/wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the admin user. + const userExists = await wallet.get('balaji'); + if (userExists) { + console.log('An identity for the client user "balaji" already exists in the wallet'); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + const enrollment = await ca.enroll({ enrollmentID: 'user1', enrollmentSecret: 'user1pw' }); + const x509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org1MSP', + type: 'X.509', + }; + await wallet.put('balaji', x509Identity); + console.log('Successfully enrolled client user "balaji" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to enroll client user "balaji": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/commercial-paper/organization/digibank/application/package.json b/commercial-paper/organization/digibank/application/package.json new file mode 100644 index 0000000..176c1ec --- /dev/null +++ b/commercial-paper/organization/digibank/application/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs", + "version": "1.0.0", + "description": "", + "main": "buy.js", + "scripts": { + "test": "rm -rf _idwallet && node addToWallet.js && node buy.js" + }, + "keywords": [], + "author": "", + "license": "Apache-2.0", + "dependencies": { + "fabric-network": "^2.2.4", + "fabric-ca-client": "^2.2.4", + "js-yaml": "^3.12.0" + }, + "devDependencies": { + "eslint": "^5.6.0" + } +} diff --git a/commercial-paper/organization/digibank/application/queryapp.js b/commercial-paper/organization/digibank/application/queryapp.js new file mode 100644 index 0000000..fcba8fa --- /dev/null +++ b/commercial-paper/organization/digibank/application/queryapp.js @@ -0,0 +1,155 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +/* + * This application has 6 basic steps: + * 1. Select an identity from a wallet + * 2. Connect to network gateway + * 3. Access PaperNet network + * 4. Construct request to query the ledger + * 5. Evaluate transactions (queries) + * 6. Process responses + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const yaml = require('js-yaml'); +const { Wallets, Gateway } = require('fabric-network'); + + +// Main program function +async function main() { + + // A wallet stores a collection of identities for use + const wallet = await Wallets.newFileSystemWallet('../identity/user/balaji/wallet'); + + + // A gateway defines the peers used to access Fabric networks + const gateway = new Gateway(); + + // Main try/catch block + try { + + // Specify userName for network access + const userName = 'balaji'; + + // Load connection profile; will be used to locate a gateway + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org1.yaml', 'utf8')); + + // Set connection options; identity and wallet + let connectionOptions = { + identity: userName, + wallet: wallet, + discovery: { enabled: true, asLocalhost: true } + + }; + + // Connect to gateway using application specified parameters + console.log('Connect to Fabric gateway.'); + + await gateway.connect(connectionProfile, connectionOptions); + + // Access PaperNet network + console.log('Use network channel: mychannel.'); + + const network = await gateway.getNetwork('mychannel'); + + // Get addressability to commercial paper contract + console.log('Use org.papernet.commercialpaper smart contract.'); + + const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper'); + + // queries - commercial paper + console.log('-----------------------------------------------------------------------------------------'); + console.log('****** Submitting commercial paper queries ****** \n\n '); + + + // 1 asset history + console.log('1. Query Commercial Paper History....'); + console.log('-----------------------------------------------------------------------------------------\n'); + let queryResponse = await contract.evaluateTransaction('queryHistory', 'MagnetoCorp', '00001'); + + let json = JSON.parse(queryResponse.toString()); + console.log(json); + console.log('\n\n'); + console.log('\n History query complete.'); + console.log('-----------------------------------------------------------------------------------------\n\n'); + + // 2 ownership query + console.log('2. Query Commercial Paper Ownership.... Papers owned by MagnetoCorp'); + console.log('-----------------------------------------------------------------------------------------\n'); + let queryResponse2 = await contract.evaluateTransaction('queryOwner', 'MagnetoCorp'); + json = JSON.parse(queryResponse2.toString()); + console.log(json); + + console.log('\n\n'); + console.log('\n Paper Ownership query complete.'); + console.log('-----------------------------------------------------------------------------------------\n\n'); + + // 3 partial key query + console.log('3. Query Commercial Paper Partial Key.... Papers in org.papernet.papers namespace and prefixed MagnetoCorp'); + console.log('-----------------------------------------------------------------------------------------\n'); + let queryResponse3 = await contract.evaluateTransaction('queryPartial', 'MagnetoCorp'); + + json = JSON.parse(queryResponse3.toString()); + console.log(json); + console.log('\n\n'); + + console.log('\n Partial Key query complete.'); + console.log('-----------------------------------------------------------------------------------------\n\n'); + + + // 4 Named query - all redeemed papers + console.log('4. Named Query: ... All papers in org.papernet.papers that are in current state of redeemed'); + console.log('-----------------------------------------------------------------------------------------\n'); + let queryResponse4 = await contract.evaluateTransaction('queryNamed', 'redeemed'); + + json = JSON.parse(queryResponse4.toString()); + console.log(json); + console.log('\n\n'); + + console.log('\n Named query "redeemed" complete.'); + console.log('-----------------------------------------------------------------------------------------\n\n'); + + + // 5 named query - by value + console.log('5. Named Query:.... All papers in org.papernet.papers with faceValue > 4000000'); + console.log('-----------------------------------------------------------------------------------------\n'); + let queryResponse5 = await contract.evaluateTransaction('queryNamed', 'value'); + + json = JSON.parse(queryResponse5.toString()); + console.log(json); + console.log('\n\n'); + + console.log('\n Named query by "value" complete.'); + console.log('-----------------------------------------------------------------------------------------\n\n'); + } catch (error) { + + console.log(`Error processing transaction. ${error}`); + console.log(error.stack); + + } finally { + + // Disconnect from the gateway + console.log('Disconnect from Fabric gateway.'); + gateway.disconnect(); + + } +} +main().then(() => { + + console.log('Queryapp program complete.'); + +}).catch((e) => { + + console.log('Queryapp program exception.'); + console.log(e); + console.log(e.stack); + process.exit(-1); + +}); diff --git a/commercial-paper/organization/digibank/application/redeem.js b/commercial-paper/organization/digibank/application/redeem.js new file mode 100644 index 0000000..0f6699e --- /dev/null +++ b/commercial-paper/organization/digibank/application/redeem.js @@ -0,0 +1,106 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +/* + * This application has 6 basic steps: + * 1. Select an identity from a wallet + * 2. Connect to network gateway + * 3. Access PaperNet network + * 4. Construct request to issue commercial paper + * 5. Submit transaction + * 6. Process response + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const yaml = require('js-yaml'); +const { Wallets, Gateway } = require('fabric-network'); +const CommercialPaper = require('../contract/lib/paper.js'); + + +// Main program function +async function main() { + + // A wallet stores a collection of identities for use + const wallet = await Wallets.newFileSystemWallet('../identity/user/balaji/wallet'); + + + // A gateway defines the peers used to access Fabric networks + const gateway = new Gateway(); + + // Main try/catch block + try { + + // Specify userName for network access + // Specify userName for network access + const userName = 'balaji'; + + // Load connection profile; will be used to locate a gateway + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org1.yaml', 'utf8')); + + // Set connection options; identity and wallet + let connectionOptions = { + identity: userName, + wallet: wallet, + discovery: { enabled:true, asLocalhost: true } + }; + + // Connect to gateway using application specified parameters + console.log('Connect to Fabric gateway.'); + + await gateway.connect(connectionProfile, connectionOptions); + + // Access PaperNet network + console.log('Use network channel: mychannel.'); + + const network = await gateway.getNetwork('mychannel'); + + // Get addressability to commercial paper contract + console.log('Use org.papernet.commercialpaper smart contract.'); + + const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper'); + + // redeem commercial paper + console.log('Submit commercial paper redeem transaction.'); + + const redeemResponse = await contract.submitTransaction('redeem', 'MagnetoCorp', '00001', 'DigiBank', 'Org2MSP', '2020-11-30'); + + // process response + console.log('Process redeem transaction response.'); + + let paper = CommercialPaper.fromBuffer(redeemResponse); + + console.log(`${paper.issuer} commercial paper : ${paper.paperNumber} successfully redeemed with ${paper.owner}`); + + console.log('Transaction complete.'); + + } catch (error) { + + console.log(`Error processing transaction. ${error}`); + console.log(error.stack); + + } finally { + + // Disconnect from the gateway + console.log('Disconnect from Fabric gateway.') + gateway.disconnect(); + + } +} +main().then(() => { + + console.log('Redeem program complete.'); + +}).catch((e) => { + + console.log('Redeem program exception.'); + console.log(e); + console.log(e.stack); + process.exit(-1); + +}); diff --git a/commercial-paper/organization/digibank/configuration/cli/docker-compose.yml b/commercial-paper/organization/digibank/configuration/cli/docker-compose.yml new file mode 100644 index 0000000..59b3885 --- /dev/null +++ b/commercial-paper/organization/digibank/configuration/cli/docker-compose.yml @@ -0,0 +1,38 @@ +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +version: '2' + +networks: + basic: + external: + name: net_test + +services: + cliDigiBank: + container_name: cliDigiBank + image: hyperledger/fabric-tools:2.0.0-beta + tty: true + environment: + - GOPATH=/opt/gopath + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + - FABRIC_LOGGING_SPEC=info + - CORE_PEER_ID=cli + - CORE_PEER_ADDRESS=peer0.org1.example.com:7051 + - CORE_PEER_LOCALMSPID=Org1MSP + - CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp + - CORE_CHAINCODE_KEEPALIVE=10 + - CORE_PEER_TLS_ENABLED=true + - CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt + - ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: /bin/bash + volumes: + - /var/run/docker.sock:/host/var/run/docker.sock + - ./../../../../organization/digibank:/opt/gopath/src/github.com/ + - ./../../../../../test-network/organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ + networks: + - basic + diff --git a/commercial-paper/organization/digibank/configuration/cli/monitordocker.sh b/commercial-paper/organization/digibank/configuration/cli/monitordocker.sh new file mode 100755 index 0000000..2cf82fb --- /dev/null +++ b/commercial-paper/organization/digibank/configuration/cli/monitordocker.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# This script uses the logspout and http stream tools to let you watch the docker containers +# in action. +# +# More information at https://github.com/gliderlabs/logspout/tree/master/httpstream + +if [ -z "$1" ]; then + DOCKER_NETWORK=basicnetwork_basic +else + DOCKER_NETWORK="$1" +fi + +if [ -z "$2" ]; then + PORT=8000 +else + PORT="$2" +fi + +echo Starting monitoring on all containers on the network ${DOCKER_NETWORK} + +docker kill logspout 2> /dev/null 1>&2 || true +docker rm logspout 2> /dev/null 1>&2 || true + +docker run -d --name="logspout" \ + --volume=/var/run/docker.sock:/var/run/docker.sock \ + --publish=127.0.0.1:${PORT}:80 \ + --network ${DOCKER_NETWORK} \ + gliderlabs/logspout +sleep 3 +curl http://127.0.0.1:${PORT}/logs diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/paper.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/paper.go new file mode 100644 index 0000000..94b072c --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/paper.go @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "encoding/json" + "fmt" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/digibank/contract-go/ledger-api" +) + +// State enum for commercial paper state property +type State uint + +const ( + // ISSUED state for when a paper has been issued + ISSUED State = iota + 1 + // TRADING state for when a paper is trading + TRADING + // REDEEMED state for when a paper has been redeemed + REDEEMED +) + +func (state State) String() string { + names := []string{"ISSUED", "TRADING", "REDEEMED"} + + if state < ISSUED || state > REDEEMED { + return "UNKNOWN" + } + + return names[state-1] +} + +// CreateCommercialPaperKey creates a key for commercial papers +func CreateCommercialPaperKey(issuer string, paperNumber string) string { + return ledgerapi.MakeKey(issuer, paperNumber) +} + +// Used for managing the fact status is private but want it in world state +type commercialPaperAlias CommercialPaper +type jsonCommercialPaper struct { + *commercialPaperAlias + State State `json:"currentState"` + Class string `json:"class"` + Key string `json:"key"` +} + +// CommercialPaper defines a commercial paper +type CommercialPaper struct { + PaperNumber string `json:"paperNumber"` + Issuer string `json:"issuer"` + IssueDateTime string `json:"issueDateTime"` + FaceValue int `json:"faceValue"` + MaturityDateTime string `json:"maturityDateTime"` + Owner string `json:"owner"` + state State `metadata:"currentState"` + class string `metadata:"class"` + key string `metadata:"key"` +} + +// UnmarshalJSON special handler for managing JSON marshalling +func (cp *CommercialPaper) UnmarshalJSON(data []byte) error { + jcp := jsonCommercialPaper{commercialPaperAlias: (*commercialPaperAlias)(cp)} + + err := json.Unmarshal(data, &jcp) + + if err != nil { + return err + } + + cp.state = jcp.State + + return nil +} + +// MarshalJSON special handler for managing JSON marshalling +func (cp CommercialPaper) MarshalJSON() ([]byte, error) { + jcp := jsonCommercialPaper{commercialPaperAlias: (*commercialPaperAlias)(&cp), State: cp.state, Class: "org.papernet.commercialpaper", Key: ledgerapi.MakeKey(cp.Issuer, cp.PaperNumber)} + + return json.Marshal(&jcp) +} + +// GetState returns the state +func (cp *CommercialPaper) GetState() State { + return cp.state +} + +// SetIssued returns the state to issued +func (cp *CommercialPaper) SetIssued() { + cp.state = ISSUED +} + +// SetTrading sets the state to trading +func (cp *CommercialPaper) SetTrading() { + cp.state = TRADING +} + +// SetRedeemed sets the state to redeemed +func (cp *CommercialPaper) SetRedeemed() { + cp.state = REDEEMED +} + +// IsIssued returns true if state is issued +func (cp *CommercialPaper) IsIssued() bool { + return cp.state == ISSUED +} + +// IsTrading returns true if state is trading +func (cp *CommercialPaper) IsTrading() bool { + return cp.state == TRADING +} + +// IsRedeemed returns true if state is redeemed +func (cp *CommercialPaper) IsRedeemed() bool { + return cp.state == REDEEMED +} + +// GetSplitKey returns values which should be used to form key +func (cp *CommercialPaper) GetSplitKey() []string { + return []string{cp.Issuer, cp.PaperNumber} +} + +// Serialize formats the commercial paper as JSON bytes +func (cp *CommercialPaper) Serialize() ([]byte, error) { + return json.Marshal(cp) +} + +// Deserialize formats the commercial paper from JSON bytes +func Deserialize(bytes []byte, cp *CommercialPaper) error { + err := json.Unmarshal(bytes, cp) + + if err != nil { + return fmt.Errorf("Error deserializing commercial paper. %s", err.Error()) + } + + return nil +} diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/paper_test.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/paper_test.go new file mode 100644 index 0000000..6af65ef --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/paper_test.go @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "testing" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/digibank/contract-go/ledger-api" + "github.com/stretchr/testify/assert" +) + +func TestString(t *testing.T) { + assert.Equal(t, "ISSUED", ISSUED.String(), "should return string for issued") + assert.Equal(t, "TRADING", TRADING.String(), "should return string for issued") + assert.Equal(t, "REDEEMED", REDEEMED.String(), "should return string for issued") + assert.Equal(t, "UNKNOWN", State(REDEEMED+1).String(), "should return unknown when not one of constants") +} + +func TestCreateCommercialPaperKey(t *testing.T) { + assert.Equal(t, ledgerapi.MakeKey("someissuer", "somepaper"), CreateCommercialPaperKey("someissuer", "somepaper"), "should return key comprised of passed values") +} + +func TestGetState(t *testing.T) { + cp := new(CommercialPaper) + cp.state = ISSUED + + assert.Equal(t, ISSUED, cp.GetState(), "should return set state") +} + +func TestSetIssued(t *testing.T) { + cp := new(CommercialPaper) + cp.SetIssued() + assert.Equal(t, ISSUED, cp.state, "should set state to trading") +} + +func TestSetTrading(t *testing.T) { + cp := new(CommercialPaper) + cp.SetTrading() + assert.Equal(t, TRADING, cp.state, "should set state to trading") +} + +func TestSetRedeemed(t *testing.T) { + cp := new(CommercialPaper) + cp.SetRedeemed() + assert.Equal(t, REDEEMED, cp.state, "should set state to trading") +} + +func TestIsIssued(t *testing.T) { + cp := new(CommercialPaper) + + cp.SetIssued() + assert.True(t, cp.IsIssued(), "should be true when status set to issued") + + cp.SetTrading() + assert.False(t, cp.IsIssued(), "should be false when status not set to issued") +} + +func TestIsTrading(t *testing.T) { + cp := new(CommercialPaper) + + cp.SetTrading() + assert.True(t, cp.IsTrading(), "should be true when status set to trading") + + cp.SetRedeemed() + assert.False(t, cp.IsTrading(), "should be false when status not set to trading") +} + +func TestIsRedeemed(t *testing.T) { + cp := new(CommercialPaper) + + cp.SetRedeemed() + assert.True(t, cp.IsRedeemed(), "should be true when status set to redeemed") + + cp.SetIssued() + assert.False(t, cp.IsRedeemed(), "should be false when status not set to redeemed") +} + +func TestGetSplitKey(t *testing.T) { + cp := new(CommercialPaper) + cp.PaperNumber = "somepaper" + cp.Issuer = "someissuer" + + assert.Equal(t, []string{"someissuer", "somepaper"}, cp.GetSplitKey(), "should return issuer and paper number as split key") +} + +func TestSerialize(t *testing.T) { + cp := new(CommercialPaper) + cp.PaperNumber = "somepaper" + cp.Issuer = "someissuer" + cp.IssueDateTime = "sometime" + cp.FaceValue = 1000 + cp.MaturityDateTime = "somelatertime" + cp.Owner = "someowner" + cp.state = TRADING + + bytes, err := cp.Serialize() + assert.Nil(t, err, "should not error on serialize") + assert.Equal(t, `{"paperNumber":"somepaper","issuer":"someissuer","issueDateTime":"sometime","faceValue":1000,"maturityDateTime":"somelatertime","owner":"someowner","currentState":2,"class":"org.papernet.commercialpaper","key":"someissuer:somepaper"}`, string(bytes), "should return JSON formatted value") +} + +func TestDeserialize(t *testing.T) { + var cp *CommercialPaper + var err error + + goodJSON := `{"paperNumber":"somepaper","issuer":"someissuer","issueDateTime":"sometime","faceValue":1000,"maturityDateTime":"somelatertime","owner":"someowner","currentState":2,"class":"org.papernet.commercialpaper","key":"someissuer:somepaper"}` + expectedCp := new(CommercialPaper) + expectedCp.PaperNumber = "somepaper" + expectedCp.Issuer = "someissuer" + expectedCp.IssueDateTime = "sometime" + expectedCp.FaceValue = 1000 + expectedCp.MaturityDateTime = "somelatertime" + expectedCp.Owner = "someowner" + expectedCp.state = TRADING + cp = new(CommercialPaper) + err = Deserialize([]byte(goodJSON), cp) + assert.Nil(t, err, "should not return error for deserialize") + assert.Equal(t, expectedCp, cp, "should create expected commercial paper") + + badJSON := `{"paperNumber":"somepaper","issuer":"someissuer","issueDateTime":"sometime","faceValue":"NaN","maturityDateTime":"somelatertime","owner":"someowner","currentState":2,"class":"org.papernet.commercialpaper","key":"someissuer:somepaper"}` + cp = new(CommercialPaper) + err = Deserialize([]byte(badJSON), cp) + assert.EqualError(t, err, "Error deserializing commercial paper. json: cannot unmarshal string into Go struct field jsonCommercialPaper.faceValue of type int", "should return error for bad data") +} diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext.go new file mode 100644 index 0000000..c346cf3 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext.go @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// TransactionContextInterface an interface to +// describe the minimum required functions for +// a transaction context in the commercial +// paper +type TransactionContextInterface interface { + contractapi.TransactionContextInterface + GetPaperList() ListInterface +} + +// TransactionContext implementation of +// TransactionContextInterface for use with +// commercial paper contract +type TransactionContext struct { + contractapi.TransactionContext + paperList *list +} + +// GetPaperList return paper list +func (tc *TransactionContext) GetPaperList() ListInterface { + if tc.paperList == nil { + tc.paperList = newList(tc) + } + + return tc.paperList +} diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext_test.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext_test.go new file mode 100644 index 0000000..81317aa --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontext_test.go @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "testing" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/digibank/contract-go/ledger-api" + "github.com/stretchr/testify/assert" +) + +func TestGetPaperList(t *testing.T) { + var tc *TransactionContext + var expectedPaperList *list + + tc = new(TransactionContext) + expectedPaperList = newList(tc) + actualList := tc.GetPaperList().(*list) + assert.Equal(t, expectedPaperList.stateList.(*ledgerapi.StateList).Name, actualList.stateList.(*ledgerapi.StateList).Name, "should configure paper list when one not already configured") + + tc = new(TransactionContext) + expectedPaperList = new(list) + expectedStateList := new(ledgerapi.StateList) + expectedStateList.Ctx = tc + expectedStateList.Name = "existing paper list" + expectedPaperList.stateList = expectedStateList + tc.paperList = expectedPaperList + assert.Equal(t, expectedPaperList, tc.GetPaperList(), "should return set paper list when already set") +} diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract.go new file mode 100644 index 0000000..4e8cee2 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract.go @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// Contract chaincode that defines +// the business logic for managing commercial +// paper +type Contract struct { + contractapi.Contract +} + +// Instantiate does nothing +func (c *Contract) Instantiate() { + fmt.Println("Instantiated") +} + +// Issue creates a new commercial paper and stores it in the world state +func (c *Contract) Issue(ctx TransactionContextInterface, issuer string, paperNumber string, issueDateTime string, maturityDateTime string, faceValue int) (*CommercialPaper, error) { + paper := CommercialPaper{PaperNumber: paperNumber, Issuer: issuer, IssueDateTime: issueDateTime, FaceValue: faceValue, MaturityDateTime: maturityDateTime, Owner: issuer} + paper.SetIssued() + + err := ctx.GetPaperList().AddPaper(&paper) + + if err != nil { + return nil, err + } + + return &paper, nil +} + +// Buy updates a commercial paper to be in trading status and sets the new owner +func (c *Contract) Buy(ctx TransactionContextInterface, issuer string, paperNumber string, currentOwner string, newOwner string, price int, purchaseDateTime string) (*CommercialPaper, error) { + paper, err := ctx.GetPaperList().GetPaper(issuer, paperNumber) + + if err != nil { + return nil, err + } + + if paper.Owner != currentOwner { + return nil, fmt.Errorf("Paper %s:%s is not owned by %s", issuer, paperNumber, currentOwner) + } + + if paper.IsIssued() { + paper.SetTrading() + } + + if !paper.IsTrading() { + return nil, fmt.Errorf("Paper %s:%s is not trading. Current state = %s", issuer, paperNumber, paper.GetState()) + } + + paper.Owner = newOwner + + err = ctx.GetPaperList().UpdatePaper(paper) + + if err != nil { + return nil, err + } + + return paper, nil +} + +// Redeem updates a commercial paper status to be redeemed +func (c *Contract) Redeem(ctx TransactionContextInterface, issuer string, paperNumber string, redeemingOwner string, redeenDateTime string) (*CommercialPaper, error) { + paper, err := ctx.GetPaperList().GetPaper(issuer, paperNumber) + + if err != nil { + return nil, err + } + + if paper.Owner != redeemingOwner { + return nil, fmt.Errorf("Paper %s:%s is not owned by %s", issuer, paperNumber, redeemingOwner) + } + + if paper.IsRedeemed() { + return nil, fmt.Errorf("Paper %s:%s is already redeemed", issuer, paperNumber) + } + + paper.Owner = paper.Issuer + paper.SetRedeemed() + + err = ctx.GetPaperList().UpdatePaper(paper) + + if err != nil { + return nil, err + } + + return paper, nil +} diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract_test.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract_test.go new file mode 100644 index 0000000..25c429b --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/papercontract_test.go @@ -0,0 +1,185 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "errors" + "testing" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ######### +// HELPERS +// ######### +type MockPaperList struct { + mock.Mock +} + +func (mpl *MockPaperList) AddPaper(paper *CommercialPaper) error { + args := mpl.Called(paper) + + return args.Error(0) +} + +func (mpl *MockPaperList) GetPaper(issuer string, papernumber string) (*CommercialPaper, error) { + args := mpl.Called(issuer, papernumber) + + return args.Get(0).(*CommercialPaper), args.Error(1) +} + +func (mpl *MockPaperList) UpdatePaper(paper *CommercialPaper) error { + args := mpl.Called(paper) + + return args.Error(0) +} + +type MockTransactionContext struct { + contractapi.TransactionContext + paperList *MockPaperList +} + +func (mtc *MockTransactionContext) GetPaperList() ListInterface { + return mtc.paperList +} + +func resetPaper(paper *CommercialPaper) { + paper.Owner = "someowner" + paper.SetTrading() +} + +// ######### +// TESTS +// ######### + +func TestIssue(t *testing.T) { + var paper *CommercialPaper + var err error + + mpl := new(MockPaperList) + ctx := new(MockTransactionContext) + ctx.paperList = mpl + + contract := new(Contract) + + var sentPaper *CommercialPaper + + mpl.On("AddPaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return paper.Issuer == "someissuer" })).Return(nil) + mpl.On("AddPaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return paper.Issuer == "someotherissuer" })).Return(errors.New("AddPaper error")) + + expectedPaper := CommercialPaper{PaperNumber: "somepaper", Issuer: "someissuer", IssueDateTime: "someissuedate", FaceValue: 1000, MaturityDateTime: "somematuritydate", Owner: "someissuer", state: 1} + paper, err = contract.Issue(ctx, "someissuer", "somepaper", "someissuedate", "somematuritydate", 1000) + assert.Nil(t, err, "should not error when add paper does not error") + assert.Equal(t, sentPaper, paper, "should send the same paper as it returns to add paper") + assert.Equal(t, expectedPaper, *paper, "should correctly configure paper") + + paper, err = contract.Issue(ctx, "someotherissuer", "somepaper", "someissuedate", "somematuritydate", 1000) + assert.EqualError(t, err, "AddPaper error", "should return error when add paper fails") + assert.Nil(t, paper, "should not return paper when fails") +} + +func TestBuy(t *testing.T) { + var paper *CommercialPaper + var err error + + mpl := new(MockPaperList) + ctx := new(MockTransactionContext) + ctx.paperList = mpl + + contract := new(Contract) + + wsPaper := new(CommercialPaper) + resetPaper(wsPaper) + + var sentPaper *CommercialPaper + var emptyPaper *CommercialPaper + shouldError := false + + mpl.On("GetPaper", "someissuer", "somepaper").Return(wsPaper, nil) + mpl.On("GetPaper", "someotherissuer", "someotherpaper").Return(emptyPaper, errors.New("GetPaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { return shouldError })).Return(errors.New("UpdatePaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return !shouldError })).Return(nil) + + paper, err = contract.Buy(ctx, "someotherissuer", "someotherpaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "GetPaper error", "should return error when GetPaper errors") + assert.Nil(t, paper, "should return nil for paper when GetPaper errors") + + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someotherowner", "someowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is not owned by someotherowner", "should error when sent owner not correct") + assert.Nil(t, paper, "should not return paper for bad owner error") + + resetPaper(wsPaper) + wsPaper.SetRedeemed() + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is not trading. Current state = REDEEMED") + assert.Nil(t, paper, "should not return paper for bad state error") + + resetPaper(wsPaper) + shouldError = true + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "UpdatePaper error", "should error when update paper fails") + assert.Nil(t, paper, "should not return paper for bad state error") + shouldError = false + + resetPaper(wsPaper) + wsPaper.SetIssued() + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.Nil(t, err, "should not error when good paper and owner") + assert.Equal(t, "someotherowner", paper.Owner, "should update the owner of the paper") + assert.True(t, paper.IsTrading(), "should mark issued paper as trading") + assert.Equal(t, sentPaper, paper, "should update same paper as it returns in the world state") +} + +func TestRedeem(t *testing.T) { + var paper *CommercialPaper + var err error + + mpl := new(MockPaperList) + ctx := new(MockTransactionContext) + ctx.paperList = mpl + + contract := new(Contract) + + var sentPaper *CommercialPaper + wsPaper := new(CommercialPaper) + resetPaper(wsPaper) + + var emptyPaper *CommercialPaper + shouldError := false + + mpl.On("GetPaper", "someissuer", "somepaper").Return(wsPaper, nil) + mpl.On("GetPaper", "someotherissuer", "someotherpaper").Return(emptyPaper, errors.New("GetPaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { return shouldError })).Return(errors.New("UpdatePaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return !shouldError })).Return(nil) + + paper, err = contract.Redeem(ctx, "someotherissuer", "someotherpaper", "someowner", "2021-12-10:10:00") + assert.EqualError(t, err, "GetPaper error", "should error when GetPaper errors") + assert.Nil(t, paper, "should not return paper when GetPaper errors") + + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someotherowner", "2021-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is not owned by someotherowner", "should error when paper owned by someone else") + assert.Nil(t, paper, "should not return paper when errors as owned by someone else") + + resetPaper(wsPaper) + wsPaper.SetRedeemed() + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someowner", "2021-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is already redeemed", "should error when paper already redeemed") + assert.Nil(t, paper, "should not return paper when errors as already redeemed") + + shouldError = true + resetPaper(wsPaper) + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someowner", "2021-12-10:10:00") + assert.EqualError(t, err, "UpdatePaper error", "should error when update paper errors") + assert.Nil(t, paper, "should not return paper when UpdatePaper errors") + shouldError = false + + resetPaper(wsPaper) + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someowner", "2021-12-10:10:00") + assert.Nil(t, err, "should not error on good redeem") + assert.True(t, paper.IsRedeemed(), "should return redeemed paper") + assert.Equal(t, sentPaper, paper, "should update same paper as it returns in the world state") +} diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist.go new file mode 100644 index 0000000..c3bdf81 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist.go @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/digibank/contract-go/ledger-api" + +// ListInterface defines functionality needed +// to interact with the world state on behalf +// of a commercial paper +type ListInterface interface { + AddPaper(*CommercialPaper) error + GetPaper(string, string) (*CommercialPaper, error) + UpdatePaper(*CommercialPaper) error +} + +type list struct { + stateList ledgerapi.StateListInterface +} + +func (cpl *list) AddPaper(paper *CommercialPaper) error { + return cpl.stateList.AddState(paper) +} + +func (cpl *list) GetPaper(issuer string, paperNumber string) (*CommercialPaper, error) { + cp := new(CommercialPaper) + + err := cpl.stateList.GetState(CreateCommercialPaperKey(issuer, paperNumber), cp) + + if err != nil { + return nil, err + } + + return cp, nil +} + +func (cpl *list) UpdatePaper(paper *CommercialPaper) error { + return cpl.stateList.UpdateState(paper) +} + +// NewList create a new list from context +func newList(ctx TransactionContextInterface) *list { + stateList := new(ledgerapi.StateList) + stateList.Ctx = ctx + stateList.Name = "org.papernet.commercialpaperlist" + stateList.Deserialize = func(bytes []byte, state ledgerapi.StateInterface) error { + return Deserialize(bytes, state.(*CommercialPaper)) + } + + list := new(list) + list.stateList = stateList + + return list +} diff --git a/commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist_test.go b/commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist_test.go new file mode 100644 index 0000000..33a2c30 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/commercial-paper/paperlist_test.go @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "errors" + "testing" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/digibank/contract-go/ledger-api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ######### +// HELPERS +// ######### + +type MockStateList struct { + mock.Mock +} + +func (msl *MockStateList) AddState(state ledgerapi.StateInterface) error { + args := msl.Called(state) + + return args.Error(0) +} + +func (msl *MockStateList) GetState(key string, state ledgerapi.StateInterface) error { + args := msl.Called(key, state) + + state.(*CommercialPaper).PaperNumber = "somepaper" + + return args.Error(0) +} + +func (msl *MockStateList) UpdateState(state ledgerapi.StateInterface) error { + args := msl.Called(state) + + return args.Error(0) +} + +// ######### +// TESTS +// ######### + +func TestAddPaper(t *testing.T) { + paper := new(CommercialPaper) + + list := new(list) + msl := new(MockStateList) + msl.On("AddState", paper).Return(errors.New("Called add state correctly")) + list.stateList = msl + + err := list.AddPaper(paper) + assert.EqualError(t, err, "Called add state correctly", "should call state list add state with paper") +} + +func TestGetPaper(t *testing.T) { + var cp *CommercialPaper + var err error + + list := new(list) + msl := new(MockStateList) + msl.On("GetState", CreateCommercialPaperKey("someissuer", "somepaper"), mock.MatchedBy(func(state ledgerapi.StateInterface) bool { _, ok := state.(*CommercialPaper); return ok })).Return(nil) + msl.On("GetState", CreateCommercialPaperKey("someotherissuer", "someotherpaper"), mock.MatchedBy(func(state ledgerapi.StateInterface) bool { _, ok := state.(*CommercialPaper); return ok })).Return(errors.New("GetState error")) + list.stateList = msl + + cp, err = list.GetPaper("someissuer", "somepaper") + assert.Nil(t, err, "should not error when get state on state list does not error") + assert.Equal(t, cp.PaperNumber, "somepaper", "should use state list GetState to fill commercial paper") + + cp, err = list.GetPaper("someotherissuer", "someotherpaper") + assert.EqualError(t, err, "GetState error", "should return error when state list get state errors") + assert.Nil(t, cp, "should not return commercial paper on error") +} + +func TestUpdatePaper(t *testing.T) { + paper := new(CommercialPaper) + + list := new(list) + msl := new(MockStateList) + msl.On("UpdateState", paper).Return(errors.New("Called update state correctly")) + list.stateList = msl + + err := list.UpdatePaper(paper) + assert.EqualError(t, err, "Called update state correctly", "should call state list update state with paper") +} + +func TestNewStateList(t *testing.T) { + ctx := new(TransactionContext) + list := newList(ctx) + stateList, ok := list.stateList.(*ledgerapi.StateList) + + assert.True(t, ok, "should make statelist of type ledgerapi.StateList") + assert.Equal(t, ctx, stateList.Ctx, "should set the context to passed context") + assert.Equal(t, "org.papernet.commercialpaperlist", stateList.Name, "should set the name for the list") + + expectedErr := Deserialize([]byte("bad json"), new(CommercialPaper)) + err := stateList.Deserialize([]byte("bad json"), new(CommercialPaper)) + assert.EqualError(t, err, expectedErr.Error(), "should call Deserialize when stateList.Deserialize called") +} diff --git a/commercial-paper/organization/digibank/contract-go/go.mod b/commercial-paper/organization/digibank/contract-go/go.mod new file mode 100644 index 0000000..4ee4f17 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/go.mod @@ -0,0 +1,13 @@ +module github.com/hyperledger/fabric-samples/commercial-paper/organization/digibank/contract-go + +go 1.13 + +require ( + github.com/go-openapi/jsonreference v0.19.3 // indirect + github.com/hyperledger/fabric-contract-api-go v1.1.0 + github.com/mailru/easyjson v0.7.0 // indirect + github.com/stretchr/testify v1.5.1 + golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 // indirect + google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 // indirect + google.golang.org/grpc v1.24.0 // indirect +) diff --git a/commercial-paper/organization/digibank/contract-go/go.sum b/commercial-paper/organization/digibank/contract-go/go.sum new file mode 100644 index 0000000..48c8221 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/go.sum @@ -0,0 +1,167 @@ +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/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +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/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +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 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +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/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/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-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-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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 h1:N66aaryRB3Ax92gH0v3hp1QYZ3zWWCCUR/j8Ifh45Ss= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/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-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-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-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-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/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-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 h1:UXl+Zk3jqqcbEVV7ace5lrt4YdA4tXiz3f/KbmD29Vo= +google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +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.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/commercial-paper/organization/digibank/contract-go/ledger-api/state.go b/commercial-paper/organization/digibank/contract-go/ledger-api/state.go new file mode 100644 index 0000000..6d8c3f8 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/ledger-api/state.go @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package ledgerapi + +import ( + "strings" +) + +// SplitKey splits a key on colon +func SplitKey(key string) []string { + return strings.Split(key, ":") +} + +// MakeKey joins key parts using colon +func MakeKey(keyParts ...string) string { + return strings.Join(keyParts, ":") +} + +// StateInterface interface states must implement +// for use in a list +type StateInterface interface { + // GetSplitKey return components that combine to form the key + GetSplitKey() []string + Serialize() ([]byte, error) +} diff --git a/commercial-paper/organization/digibank/contract-go/ledger-api/statelist.go b/commercial-paper/organization/digibank/contract-go/ledger-api/statelist.go new file mode 100644 index 0000000..492efb3 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/ledger-api/statelist.go @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package ledgerapi + +import ( + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// StateListInterface functions that a state list +// should have +type StateListInterface interface { + AddState(StateInterface) error + GetState(string, StateInterface) error + UpdateState(StateInterface) error +} + +// StateList useful for managing putting data in and out +// of the ledger. Implementation of StateListInterface +type StateList struct { + Ctx contractapi.TransactionContextInterface + Name string + Deserialize func([]byte, StateInterface) error +} + +// AddState puts state into world state +func (sl *StateList) AddState(state StateInterface) error { + key, _ := sl.Ctx.GetStub().CreateCompositeKey(sl.Name, state.GetSplitKey()) + data, err := state.Serialize() + + if err != nil { + return err + } + + return sl.Ctx.GetStub().PutState(key, data) +} + +// GetState returns state from world state. Unmarshalls the JSON +// into passed state. Key is the split key value used in Add/Update +// joined using a colon +func (sl *StateList) GetState(key string, state StateInterface) error { + ledgerKey, _ := sl.Ctx.GetStub().CreateCompositeKey(sl.Name, SplitKey(key)) + data, err := sl.Ctx.GetStub().GetState(ledgerKey) + + if err != nil { + return err + } else if data == nil { + return fmt.Errorf("No state found for %s", key) + } + + return sl.Deserialize(data, state) +} + +// UpdateState puts state into world state. Same as AddState but +// separate as semantically different +func (sl *StateList) UpdateState(state StateInterface) error { + return sl.AddState(state) +} diff --git a/commercial-paper/organization/digibank/contract-go/main.go b/commercial-paper/organization/digibank/contract-go/main.go new file mode 100644 index 0000000..002c4f9 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-go/main.go @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/hyperledger/fabric-samples/commercial-paper/organization/digibank/contract-go/commercial-paper" +) + +func main() { + + contract := new(commercialpaper.Contract) + contract.TransactionContextHandler = new(commercialpaper.TransactionContext) + contract.Name = "org.papernet.commercialpaper" + contract.Info.Version = "0.0.1" + + chaincode, err := contractapi.NewChaincode(contract) + + if err != nil { + panic(fmt.Sprintf("Error creating chaincode. %s", err.Error())) + } + + chaincode.Info.Title = "CommercialPaperChaincode" + chaincode.Info.Version = "0.0.1" + + err = chaincode.Start() + + if err != nil { + panic(fmt.Sprintf("Error starting chaincode. %s", err.Error())) + } +} diff --git a/commercial-paper/organization/digibank/contract-java/.gitignore b/commercial-paper/organization/digibank/contract-java/.gitignore new file mode 100644 index 0000000..ae1478c --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/.gitignore @@ -0,0 +1,10 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +/.classpath +/.gradle/ +/.project +/.settings/ +/bin/ +/build/ diff --git a/commercial-paper/organization/digibank/contract-java/build.gradle b/commercial-paper/organization/digibank/contract-java/build.gradle new file mode 100644 index 0000000..c0a2b43 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'java-library-distribution' +} + +version '0.0.1' + +sourceCompatibility = 1.8 + +repositories { + mavenLocal() + mavenCentral() + maven { + url 'https://jitpack.io' + } + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + +} + +dependencies { + compileOnly group: 'org.hyperledger.fabric-chaincode-java', name: 'fabric-chaincode-shim', version: '2.+' + compile group: 'org.json', name: 'json', version: '20180813' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-parameters" +} diff --git a/commercial-paper/organization/digibank/contract-java/gradle/wrapper/gradle-wrapper.jar b/commercial-paper/organization/digibank/contract-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/commercial-paper/organization/digibank/contract-java/gradlew.bat b/commercial-paper/organization/digibank/contract-java/gradlew.bat new file mode 100644 index 0000000..24467a1 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/commercial-paper/organization/digibank/contract-java/settings.gradle b/commercial-paper/organization/digibank/contract-java/settings.gradle new file mode 100644 index 0000000..0c5f072 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'papercontract' + diff --git a/commercial-paper/organization/digibank/contract-java/shadow-build.gradle b/commercial-paper/organization/digibank/contract-java/shadow-build.gradle new file mode 100644 index 0000000..0a29887 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/shadow-build.gradle @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +plugins { + id 'com.github.johnrengelman.shadow' version '5.1.0' + id 'java' +} + + +version '0.0.1' + +sourceCompatibility = 1.8 + +repositories { + mavenLocal() + mavenCentral() + maven { + url 'https://jitpack.io' + } +} + +dependencies { + implementation group: 'org.hyperledger.fabric-chaincode-java', name: 'fabric-chaincode-shim', version: '2.+' + implementation group: 'org.json', name: 'json', version: '20180813' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +shadowJar { + baseName = 'chaincode' + version = null + classifier = null + + manifest { + attributes 'Main-Class': 'org.hyperledger.fabric.contract.ContractRouter' + } +} + + +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-parameters" +} diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaper.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaper.java new file mode 100644 index 0000000..13d16b6 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaper.java @@ -0,0 +1,183 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.example; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.example.ledgerapi.State; +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; +import org.json.JSONObject; +import org.json.JSONPropertyIgnore; + +@DataType() +public class CommercialPaper extends State { + + // Enumerate commercial paper state values + public final static String ISSUED = "ISSUED"; + public final static String TRADING = "TRADING"; + public final static String REDEEMED = "REDEEMED"; + + @Property() + private String state=""; + + public String getState() { + return state; + } + + public CommercialPaper setState(String state) { + this.state = state; + return this; + } + + @JSONPropertyIgnore() + public boolean isIssued() { + return this.state.equals(CommercialPaper.ISSUED); + } + + @JSONPropertyIgnore() + public boolean isTrading() { + return this.state.equals(CommercialPaper.TRADING); + } + + @JSONPropertyIgnore() + public boolean isRedeemed() { + return this.state.equals(CommercialPaper.REDEEMED); + } + + public CommercialPaper setIssued() { + this.state = CommercialPaper.ISSUED; + return this; + } + + public CommercialPaper setTrading() { + this.state = CommercialPaper.TRADING; + return this; + } + + public CommercialPaper setRedeemed() { + this.state = CommercialPaper.REDEEMED; + return this; + } + + @Property() + private String paperNumber; + + @Property() + private String issuer; + + @Property() + private String issueDateTime; + + @Property() + private int faceValue; + + @Property() + private String maturityDateTime; + + @Property() + private String owner; + + public String getOwner() { + return owner; + } + + public CommercialPaper setOwner(String owner) { + this.owner = owner; + return this; + } + + public CommercialPaper() { + super(); + } + + public CommercialPaper setKey() { + this.key = State.makeKey(new String[] { this.paperNumber }); + return this; + } + + public String getPaperNumber() { + return paperNumber; + } + + public CommercialPaper setPaperNumber(String paperNumber) { + this.paperNumber = paperNumber; + return this; + } + + public String getIssuer() { + return issuer; + } + + public CommercialPaper setIssuer(String issuer) { + this.issuer = issuer; + return this; + } + + public String getIssueDateTime() { + return issueDateTime; + } + + public CommercialPaper setIssueDateTime(String issueDateTime) { + this.issueDateTime = issueDateTime; + return this; + } + + public int getFaceValue() { + return faceValue; + } + + public CommercialPaper setFaceValue(int faceValue) { + this.faceValue = faceValue; + return this; + } + + public String getMaturityDateTime() { + return maturityDateTime; + } + + public CommercialPaper setMaturityDateTime(String maturityDateTime) { + this.maturityDateTime = maturityDateTime; + return this; + } + + @Override + public String toString() { + return "Paper::" + this.key + " " + this.getPaperNumber() + " " + getIssuer() + " " + getFaceValue(); + } + + /** + * Deserialize a state data to commercial paper + * + * @param {Buffer} data to form back into the object + */ + public static CommercialPaper deserialize(byte[] data) { + JSONObject json = new JSONObject(new String(data, UTF_8)); + + String issuer = json.getString("issuer"); + String paperNumber = json.getString("paperNumber"); + String issueDateTime = json.getString("issueDateTime"); + String maturityDateTime = json.getString("maturityDateTime"); + String owner = json.getString("owner"); + int faceValue = json.getInt("faceValue"); + String state = json.getString("state"); + return createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue,owner,state); + } + + public static byte[] serialize(CommercialPaper paper) { + return State.serialize(paper); + } + + /** + * Factory method to create a commercial paper object + */ + public static CommercialPaper createInstance(String issuer, String paperNumber, String issueDateTime, + String maturityDateTime, int faceValue, String owner, String state) { + return new CommercialPaper().setIssuer(issuer).setPaperNumber(paperNumber).setMaturityDateTime(maturityDateTime) + .setFaceValue(faceValue).setKey().setIssueDateTime(issueDateTime).setOwner(owner).setState(state); + } + + +} diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContext.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContext.java new file mode 100644 index 0000000..7a946f2 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContext.java @@ -0,0 +1,15 @@ +package org.example; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeStub; + +class CommercialPaperContext extends Context { + + public CommercialPaperContext(ChaincodeStub stub) { + super(stub); + this.paperList = new PaperList(this); + } + + public PaperList paperList; + +} \ No newline at end of file diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContract.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContract.java new file mode 100644 index 0000000..a75f47e --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/CommercialPaperContract.java @@ -0,0 +1,170 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ +package org.example; + +import java.util.logging.Logger; + +import org.example.ledgerapi.State; +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contact; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.shim.ChaincodeStub; + +/** + * A custom context provides easy access to list of all commercial papers + */ + +/** + * Define commercial paper smart contract by extending Fabric Contract class + * + */ +@Contract(name = "org.papernet.commercialpaper", info = @Info(title = "MyAsset contract", description = "", version = "0.0.1", license = @License(name = "SPDX-License-Identifier: ", url = ""), contact = @Contact(email = "java-contract@example.com", name = "java-contract", url = "http://java-contract.me"))) +@Default +public class CommercialPaperContract implements ContractInterface { + + // use the classname for the logger, this way you can refactor + private final static Logger LOG = Logger.getLogger(CommercialPaperContract.class.getName()); + + @Override + public Context createContext(ChaincodeStub stub) { + return new CommercialPaperContext(stub); + } + + public CommercialPaperContract() { + + } + + /** + * Define a custom context for commercial paper + */ + + /** + * Instantiate to perform any setup of the ledger that might be required. + * + * @param {Context} ctx the transaction context + */ + @Transaction + public void instantiate(CommercialPaperContext ctx) { + // No implementation required with this example + // It could be where data migration is performed, if necessary + LOG.info("No data migration to perform"); + } + + /** + * Issue commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} issueDateTime paper issue date + * @param {String} maturityDateTime paper maturity date + * @param {Integer} faceValue face value of paper + */ + @Transaction + public CommercialPaper issue(CommercialPaperContext ctx, String issuer, String paperNumber, String issueDateTime, + String maturityDateTime, int faceValue) { + + System.out.println(ctx); + + // create an instance of the paper + CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, + faceValue,issuer,""); + + // Smart contract, rather than paper, moves paper into ISSUED state + paper.setIssued(); + + // Newly issued paper is owned by the issuer + paper.setOwner(issuer); + + System.out.println(paper); + // Add the paper to the list of all similar commercial papers in the ledger + // world state + ctx.paperList.addPaper(paper); + + // Must return a serialized paper to caller of smart contract + return paper; + } + + /** + * Buy commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} currentOwner current owner of paper + * @param {String} newOwner new owner of paper + * @param {Integer} price price paid for this paper + * @param {String} purchaseDateTime time paper was purchased (i.e. traded) + */ + @Transaction + public CommercialPaper buy(CommercialPaperContext ctx, String issuer, String paperNumber, String currentOwner, + String newOwner, int price, String purchaseDateTime) { + + // Retrieve the current paper using key fields provided + String paperKey = State.makeKey(new String[] { paperNumber }); + CommercialPaper paper = ctx.paperList.getPaper(paperKey); + + // Validate current owner + if (!paper.getOwner().equals(currentOwner)) { + throw new RuntimeException("Paper " + issuer + paperNumber + " is not owned by " + currentOwner); + } + + // First buy moves state from ISSUED to TRADING + if (paper.isIssued()) { + paper.setTrading(); + } + + // Check paper is not already REDEEMED + if (paper.isTrading()) { + paper.setOwner(newOwner); + } else { + throw new RuntimeException( + "Paper " + issuer + paperNumber + " is not trading. Current state = " + paper.getState()); + } + + // Update the paper + ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * Redeem commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} redeemingOwner redeeming owner of paper + * @param {String} redeemDateTime time paper was redeemed + */ + @Transaction + public CommercialPaper redeem(CommercialPaperContext ctx, String issuer, String paperNumber, String redeemingOwner, + String redeemDateTime) { + + String paperKey = CommercialPaper.makeKey(new String[] { paperNumber }); + + CommercialPaper paper = ctx.paperList.getPaper(paperKey); + + // Check paper is not REDEEMED + if (paper.isRedeemed()) { + throw new RuntimeException("Paper " + issuer + paperNumber + " already redeemed"); + } + + // Verify that the redeemer owns the commercial paper before redeeming it + if (paper.getOwner().equals(redeemingOwner)) { + paper.setOwner(paper.getIssuer()); + paper.setRedeemed(); + } else { + throw new RuntimeException("Redeeming owner does not own paper" + issuer + paperNumber); + } + + ctx.paperList.updatePaper(paper); + return paper; + } + +} diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/PaperList.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/PaperList.java new file mode 100644 index 0000000..0ecd24c --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/PaperList.java @@ -0,0 +1,31 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.example; + +import org.example.ledgerapi.StateList; +import org.hyperledger.fabric.contract.Context; + +public class PaperList { + + private StateList stateList; + + public PaperList(Context ctx) { + this.stateList = StateList.getStateList(ctx, PaperList.class.getSimpleName(), CommercialPaper::deserialize); + } + + public PaperList addPaper(CommercialPaper paper) { + stateList.addState(paper); + return this; + } + + public CommercialPaper getPaper(String paperKey) { + return (CommercialPaper) this.stateList.getState(paperKey); + } + + public PaperList updatePaper(CommercialPaper paper) { + this.stateList.updateState(paper); + return this; + } +} diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/State.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/State.java new file mode 100644 index 0000000..46d35c3 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/State.java @@ -0,0 +1,60 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ +package org.example.ledgerapi; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.json.JSONObject; + +/** + * State class. States have a class, unique key, and a lifecycle current state + * the current state is determined by the specific subclass + */ +public class State { + + protected String key; + + /** + * @param {String|Object} class An identifiable class of the instance + * @param {keyParts[]} elements to pull together to make a key for the objects + */ + public State() { + + } + + String getKey() { + return this.key; + } + + public String[] getSplitKey() { + return State.splitKey(this.key); + } + + /** + * Convert object to buffer containing JSON data serialization Typically used + * before putState()ledger API + * + * @param {Object} JSON object to serialize + * @return {buffer} buffer with the data to store + */ + public static byte[] serialize(Object object) { + String jsonStr = new JSONObject(object).toString(); + return jsonStr.getBytes(UTF_8); + } + + /** + * Join the keyParts to make a unififed string + * + * @param (String[]) keyParts + */ + public static String makeKey(String[] keyParts) { + return String.join(":", keyParts); + } + + public static String[] splitKey(String key) { + System.out.println("splitting key " + key + " " + java.util.Arrays.asList(key.split(":"))); + return key.split(":"); + } + +} diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java new file mode 100644 index 0000000..891788e --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java @@ -0,0 +1,6 @@ +package org.example.ledgerapi; + +@FunctionalInterface +public interface StateDeserializer { + State deserialize(byte[] buffer); +} diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateList.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateList.java new file mode 100644 index 0000000..d672586 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/StateList.java @@ -0,0 +1,48 @@ +package org.example.ledgerapi; + +import org.example.ledgerapi.impl.StateListImpl; +import org.hyperledger.fabric.contract.Context; + +public interface StateList { + + /* + * SPDX-License-Identifier: Apache-2.0 + */ + + /** + * StateList provides a named virtual container for a set of ledger states. Each + * state has a unique key which associates it with the container, rather than + * the container containing a link to the state. This minimizes collisions for + * parallel transactions on different states. + */ + + /** + * Store Fabric context for subsequent API access, and name of list + */ + static StateList getStateList(Context ctx, String listName, StateDeserializer deserializer) { + return new StateListImpl(ctx, listName, deserializer); + } + + /** + * Add a state to the list. Creates a new state in worldstate with appropriate + * composite key. Note that state defines its own key. State object is + * serialized before writing. + */ + public StateList addState(State state); + + /** + * Get a state from the list using supplied keys. Form composite keys to + * retrieve state from world state. State data is deserialized into JSON object + * before being returned. + */ + public State getState(String key); + + /** + * Update a state in the list. Puts the new state in world state with + * appropriate composite key. Note that state defines its own key. A state is + * serialized before writing. Logic is very similar to addState() but kept + * separate becuase it is semantically distinct. + */ + public StateList updateState(State state); + +} diff --git a/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java new file mode 100644 index 0000000..78a4293 --- /dev/null +++ b/commercial-paper/organization/digibank/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java @@ -0,0 +1,100 @@ +package org.example.ledgerapi.impl; + +import java.util.Arrays; + +import org.example.ledgerapi.State; +import org.example.ledgerapi.StateDeserializer; +import org.example.ledgerapi.StateList; +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.CompositeKey; + +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * StateList provides a named virtual container for a set of ledger states. Each + * state has a unique key which associates it with the container, rather than + * the container containing a link to the state. This minimizes collisions for + * parallel transactions on different states. + */ +public class StateListImpl implements StateList { + + private Context ctx; + private String name; + private Object supportedClasses; + private StateDeserializer deserializer; + + /** + * Store Fabric context for subsequent API access, and name of list + * + * @param deserializer + */ + public StateListImpl(Context ctx, String listName, StateDeserializer deserializer) { + this.ctx = ctx; + this.name = listName; + this.deserializer = deserializer; + + } + + /** + * Add a state to the list. Creates a new state in worldstate with appropriate + * composite key. Note that state defines its own key. State object is + * serialized before writing. + */ + @Override + public StateList addState(State state) { + System.out.println("Adding state " + this.name); + ChaincodeStub stub = this.ctx.getStub(); + System.out.println("Stub=" + stub); + String[] splitKey = state.getSplitKey(); + System.out.println("Split key " + Arrays.asList(splitKey)); + + CompositeKey ledgerKey = stub.createCompositeKey(this.name, splitKey); + System.out.println("ledgerkey is "); + System.out.println(ledgerKey); + + byte[] data = State.serialize(state); + System.out.println("ctx" + this.ctx); + System.out.println("stub" + this.ctx.getStub()); + this.ctx.getStub().putState(ledgerKey.toString(), data); + + return this; + } + + /** + * Get a state from the list using supplied keys. Form composite keys to + * retrieve state from world state. State data is deserialized into JSON object + * before being returned. + */ + @Override + public State getState(String key) { + + CompositeKey ledgerKey = this.ctx.getStub().createCompositeKey(this.name, State.splitKey(key)); + + byte[] data = this.ctx.getStub().getState(ledgerKey.toString()); + if (data != null) { + State state = this.deserializer.deserialize(data); + return state; + } else { + return null; + } + } + + /** + * Update a state in the list. Puts the new state in world state with + * appropriate composite key. Note that state defines its own key. A state is + * serialized before writing. Logic is very similar to addState() but kept + * separate becuase it is semantically distinct. + */ + @Override + public StateList updateState(State state) { + CompositeKey ledgerKey = this.ctx.getStub().createCompositeKey(this.name, state.getSplitKey()); + byte[] data = State.serialize(state); + this.ctx.getStub().putState(ledgerKey.toString(), data); + + return this; + } + +} diff --git a/commercial-paper/organization/digibank/contract/.editorconfig b/commercial-paper/organization/digibank/contract/.editorconfig new file mode 100755 index 0000000..75a13be --- /dev/null +++ b/commercial-paper/organization/digibank/contract/.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/commercial-paper/organization/digibank/contract/.eslintignore b/commercial-paper/organization/digibank/contract/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/commercial-paper/organization/digibank/contract/.eslintrc.js b/commercial-paper/organization/digibank/contract/.eslintrc.js new file mode 100644 index 0000000..6772c66 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/.eslintrc.js @@ -0,0 +1,37 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +module.exports = { + env: { + node: 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'] + } +}; diff --git a/commercial-paper/organization/digibank/contract/.npmignore b/commercial-paper/organization/digibank/contract/.npmignore new file mode 100644 index 0000000..a00ca94 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/.npmignore @@ -0,0 +1,77 @@ +# +# 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/ + +# 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/commercial-paper/organization/digibank/contract/index.js b/commercial-paper/organization/digibank/contract/index.js new file mode 100644 index 0000000..e0ea2c7 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/index.js @@ -0,0 +1,10 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const cpcontract = require('./lib/papercontract.js'); +module.exports.contracts = [cpcontract]; diff --git a/commercial-paper/organization/digibank/contract/ledger-api/state.js b/commercial-paper/organization/digibank/contract/ledger-api/state.js new file mode 100644 index 0000000..f63e792 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/ledger-api/state.js @@ -0,0 +1,105 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +/** + * State class. States have a class, unique key, and a lifecycle current state + * the current state is determined by the specific subclass + */ +class State { + + /** + * @param {String|Object} class An indentifiable class of the instance + * @param {keyParts[]} elements to pull together to make a key for the objects + */ + constructor(stateClass, keyParts) { + this.class = stateClass; + this.key = State.makeKey(keyParts); + this.currentState = null; + } + + getClass() { + return this.class; + } + + getKey() { + return this.key; + } + + getSplitKey(){ + return State.splitKey(this.key); + } + + getCurrentState(){ + return this.currentState; + } + + // not used +/* serialize() { + + return State.serialize(this); + } */ + + /** + * Convert object to buffer containing JSON data serialization + * Typically used before putState()ledger API + * @param {Object} JSON object to serialize + * @return {buffer} buffer with the data to store + */ + static serialize(object) { + // don't write the key:value passed in - we already have a real composite key, issuer and paper Number. + delete object.key; + return Buffer.from(JSON.stringify(object)); + } + + /** + * Deserialize object into one of a set of supported JSON classes + * i.e. Covert serialized data to JSON object + * Typically used after getState() ledger API + * @param {data} data to deserialize into JSON object + * @param (supportedClasses) the set of classes data can be serialized to + * @return {json} json with the data to store + */ + static deserialize(data, supportedClasses) { + let json = JSON.parse(data.toString()); + let objClass = supportedClasses[json.class]; + if (!objClass) { + throw new Error(`Unknown class of ${json.class}`); + } + let object = new (objClass)(json); + + return object; + } + + /** + * Deserialize object into specific object class + * Typically used after getState() ledger API + * @param {data} data to deserialize into JSON object + * @return {json} json with the data to store + */ + static deserializeClass(data, objClass) { + let json = JSON.parse(data.toString()); + let object = new (objClass)(json); + return object; + } + + /** + * Join the keyParts to make a unififed string + * @param (String[]) keyParts + */ + static makeKey(keyParts) { + // return keyParts.map(part => JSON.stringify(part)).join(':'); + return keyParts.map(part => part).join(':'); + } + + static splitKey(key){ + return key.split(':'); + } + +} + +module.exports = State; diff --git a/commercial-paper/organization/digibank/contract/ledger-api/statelist.js b/commercial-paper/organization/digibank/contract/ledger-api/statelist.js new file mode 100644 index 0000000..15ec837 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/ledger-api/statelist.js @@ -0,0 +1,74 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; +const State = require('./state.js'); + +/** + * StateList provides a named virtual container for a set of ledger states. + * Each state has a unique key which associates it with the container, rather + * than the container containing a link to the state. This minimizes collisions + * for parallel transactions on different states. + */ +class StateList { + + /** + * Store Fabric context for subsequent API access, and name of list + */ + constructor(ctx, listName) { + this.ctx = ctx; + this.name = listName; + this.supportedClasses = {}; + + } + + /** + * Add a state to the list. Creates a new state in worldstate with + * appropriate composite key. Note that state defines its own key. + * State object is serialized before writing. + */ + async addState(state) { + let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey()); + let data = State.serialize(state); + await this.ctx.stub.putState(key, data); + } + + /** + * Get a state from the list using supplied keys. Form composite + * keys to retrieve state from world state. State data is deserialized + * into JSON object before being returned. + */ + async getState(key) { + let ledgerKey = this.ctx.stub.createCompositeKey(this.name, State.splitKey(key)); + let data = await this.ctx.stub.getState(ledgerKey); + if (data && data.toString('utf8')) { + let state = State.deserialize(data, this.supportedClasses); + return state; + } else { + return null; + } + } + + /** + * Update a state in the list. Puts the new state in world state with + * appropriate composite key. Note that state defines its own key. + * A state is serialized before writing. Logic is very similar to + * addState() but kept separate becuase it is semantically distinct. + */ + async updateState(state) { + let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey()); + let data = State.serialize(state); + await this.ctx.stub.putState(key, data); + } + + /** Stores the class for future deserialization */ + use(stateClass) { + this.supportedClasses[stateClass.getClass()] = stateClass; + } + +} + +module.exports = StateList; diff --git a/commercial-paper/organization/digibank/contract/lib/paper.js b/commercial-paper/organization/digibank/contract/lib/paper.js new file mode 100644 index 0000000..0629ba7 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/lib/paper.js @@ -0,0 +1,121 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Utility class for ledger state +const State = require('./../ledger-api/state.js'); + +// Enumerate commercial paper state values +const cpState = { + ISSUED: 1, + PENDING: 2, + TRADING: 3, + REDEEMED: 4 +}; + +/** + * CommercialPaper class extends State class + * Class will be used by application and smart contract to define a paper + */ +class CommercialPaper extends State { + + constructor(obj) { + super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]); + Object.assign(this, obj); + } + + /** + * Basic getters and setters + */ + getIssuer() { + return this.issuer; + } + + setIssuer(newIssuer) { + this.issuer = newIssuer; + } + + getOwner() { + return this.owner; + } + + setOwnerMSP(mspid) { + this.mspid = mspid; + } + + getOwnerMSP() { + return this.mspid; + } + + setOwner(newOwner) { + this.owner = newOwner; + } + + /** + * Useful methods to encapsulate commercial paper states + */ + setIssued() { + this.currentState = cpState.ISSUED; + } + + setTrading() { + this.currentState = cpState.TRADING; + } + + setRedeemed() { + this.currentState = cpState.REDEEMED; + } + + setPending() { + this.currentState = cpState.PENDING; + } + + isIssued() { + return this.currentState === cpState.ISSUED; + } + + isTrading() { + return this.currentState === cpState.TRADING; + } + + isRedeemed() { + return this.currentState === cpState.REDEEMED; + } + + isPending() { + return this.currentState === cpState.PENDING; + } + + static fromBuffer(buffer) { + return CommercialPaper.deserialize(buffer); + } + + toBuffer() { + return Buffer.from(JSON.stringify(this)); + } + + /** + * Deserialize a state data to commercial paper + * @param {Buffer} data to form back into the object + */ + static deserialize(data) { + return State.deserializeClass(data, CommercialPaper); + } + + /** + * Factory method to create a commercial paper object + */ + static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) { + return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue }); + } + + static getClass() { + return 'org.papernet.commercialpaper'; + } +} + +module.exports = CommercialPaper; diff --git a/commercial-paper/organization/digibank/contract/lib/papercontract.js b/commercial-paper/organization/digibank/contract/lib/papercontract.js new file mode 100644 index 0000000..c3d2cdb --- /dev/null +++ b/commercial-paper/organization/digibank/contract/lib/papercontract.js @@ -0,0 +1,339 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Fabric smart contract classes +const { Contract, Context } = require('fabric-contract-api'); + +// PaperNet specifc classes +const CommercialPaper = require('./paper.js'); +const PaperList = require('./paperlist.js'); +const QueryUtils = require('./queries.js'); + +/** + * A custom context provides easy access to list of all commercial papers + */ +class CommercialPaperContext extends Context { + + constructor() { + super(); + // All papers are held in a list of papers + this.paperList = new PaperList(this); + } + +} + +/** + * Define commercial paper smart contract by extending Fabric Contract class + * + */ +class CommercialPaperContract extends Contract { + + constructor() { + // Unique namespace when multiple contracts per chaincode file + super('org.papernet.commercialpaper'); + } + + /** + * Define a custom context for commercial paper + */ + createContext() { + return new CommercialPaperContext(); + } + + /** + * Instantiate to perform any setup of the ledger that might be required. + * @param {Context} ctx the transaction context + */ + async instantiate(ctx) { + // No implementation required with this example + // It could be where data migration is performed, if necessary + console.log('Instantiate the contract'); + } + + /** + * Issue commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} issueDateTime paper issue date + * @param {String} maturityDateTime paper maturity date + * @param {Integer} faceValue face value of paper + */ + async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) { + + // create an instance of the paper + let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, parseInt(faceValue)); + + // Smart contract, rather than paper, moves paper into ISSUED state + paper.setIssued(); + + // save the owner's MSP + let mspid = ctx.clientIdentity.getMSPID(); + paper.setOwnerMSP(mspid); + + // Newly issued paper is owned by the issuer to begin with (recorded for reporting purposes) + paper.setOwner(issuer); + + // Add the paper to the list of all similar commercial papers in the ledger world state + await ctx.paperList.addPaper(paper); + + // Must return a serialized paper to caller of smart contract + return paper; + } + + /** + * Buy commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} currentOwner current owner of paper + * @param {String} newOwner new owner of paper + * @param {Integer} price price paid for this paper // transaction input - not written to asset + * @param {String} purchaseDateTime time paper was purchased (i.e. traded) // transaction input - not written to asset + */ + async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) { + + // Retrieve the current paper using key fields provided + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + let paper = await ctx.paperList.getPaper(paperKey); + + // Validate current owner + if (paper.getOwner() !== currentOwner) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not owned by ' + currentOwner); + } + + // First buy moves state from ISSUED to TRADING (when running ) + if (paper.isIssued()) { + paper.setTrading(); + } + + // Check paper is not already REDEEMED + if (paper.isTrading()) { + paper.setOwner(newOwner); + // save the owner's MSP + let mspid = ctx.clientIdentity.getMSPID(); + paper.setOwnerMSP(mspid); + } else { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not trading. Current state = ' + paper.getCurrentState()); + } + + // Update the paper + await ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * Buy request: (2-phase confirmation: Commercial paper is 'PENDING' subject to completion of transfer by owning org) + * Alternative to 'buy' transaction + * Note: 'buy_request' puts paper in 'PENDING' state - subject to transfer confirmation [below]. + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} currentOwner current owner of paper + * @param {String} newOwner new owner of paper // transaction input - not written to asset per se - but written to block + * @param {Integer} price price paid for this paper // transaction input - not written to asset per se - but written to block + * @param {String} purchaseDateTime time paper was requested // transaction input - ditto. + */ + async buy_request(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) { + + + // Retrieve the current paper using key fields provided + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + let paper = await ctx.paperList.getPaper(paperKey); + + // Validate current owner - this is really information for the user trying the sample, rather than any 'authorisation' check per se FYI + if (paper.getOwner() !== currentOwner) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not owned by ' + currentOwner + ' provided as a paraneter'); + } + // paper set to 'PENDING' - can only be transferred (confirmed) by identity from owning org (MSP check). + paper.setPending(); + + // Update the paper + await ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * transfer commercial paper: only the owning org has authority to execute. It is the complement to the 'buy_request' transaction. '[]' is optional below. + * eg. issue -> buy_request -> transfer -> [buy ...n | [buy_request...n | transfer ...n] ] -> redeem + * this transaction 'pair' is an alternative to the straight issue -> buy -> [buy....n] -> redeem ...path + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} newOwner new owner of paper + * @param {String} newOwnerMSP MSP id of the transferee + * @param {String} confirmDateTime confirmed transfer date. + */ + async transfer(ctx, issuer, paperNumber, newOwner, newOwnerMSP, confirmDateTime) { + + // Retrieve the current paper using key fields provided + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + let paper = await ctx.paperList.getPaper(paperKey); + + // Validate current owner's MSP in the paper === invoking transferor's MSP id - can only transfer if you are the owning org. + + if (paper.getOwnerMSP() !== ctx.clientIdentity.getMSPID()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not owned by the current invoking Organisation, and not authorised to transfer'); + } + + // Paper needs to be 'pending' - which means you need to have run 'buy_pending' transaction first. + if ( ! paper.isPending()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not currently in state: PENDING for transfer to occur: \n must run buy_request transaction first'); + } + // else all good + + paper.setOwner(newOwner); + // set the MSP of the transferee (so that, that org may also pass MSP check, if subsequently transferred/sold on) + paper.setOwnerMSP(newOwnerMSP); + paper.setTrading(); + paper.confirmDateTime = confirmDateTime; + + // Update the paper + await ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * Redeem commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} redeemingOwner redeeming owner of paper + * @param {String} issuingOwnerMSP the MSP of the org that the paper will be redeemed with. + * @param {String} redeemDateTime time paper was redeemed + */ + async redeem(ctx, issuer, paperNumber, redeemingOwner, issuingOwnerMSP, redeemDateTime) { + + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + + let paper = await ctx.paperList.getPaper(paperKey); + + // Check paper is not alread in a state of REDEEMED + if (paper.isRedeemed()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' has already been redeemed'); + } + + // Validate current redeemer's MSP matches the invoking redeemer's MSP id - can only redeem if you are the owning org. + + if (paper.getOwnerMSP() !== ctx.clientIdentity.getMSPID()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' cannot be redeemed by ' + ctx.clientIdentity.getMSPID() + ', as it is not the authorised owning Organisation'); + } + + // As this is just a sample, can show additional verification check: that the redeemer provided matches that on record, before redeeming it + if (paper.getOwner() === redeemingOwner) { + paper.setOwner(paper.getIssuer()); + paper.setOwnerMSP(issuingOwnerMSP); + paper.setRedeemed(); + paper.redeemDateTime = redeemDateTime; // record redemption date against the asset (the complement to 'issue date') + } else { + throw new Error('\nRedeeming owner: ' + redeemingOwner + ' organisation does not currently own paper: ' + issuer + paperNumber); + } + + await ctx.paperList.updatePaper(paper); + return paper; + } + + // Query transactions + + /** + * Query history of a commercial paper + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + */ + async queryHistory(ctx, issuer, paperNumber) { + + // Get a key to be used for History query + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let results = await query.getAssetHistory(issuer, paperNumber); // (cpKey); + return results; + + } + + /** + * queryOwner commercial paper: supply name of owning org, to find list of papers based on owner field + * @param {Context} ctx the transaction context + * @param {String} owner commercial paper owner + */ + async queryOwner(ctx, owner) { + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let owner_results = await query.queryKeyByOwner(owner); + + return owner_results; + } + + /** + * queryPartial commercial paper - provide a prefix eg. "DigiBank" will list all papers _issued_ by DigiBank etc etc + * @param {Context} ctx the transaction context + * @param {String} prefix asset class prefix (added to paperlist namespace) eg. org.papernet.paperMagnetoCorp asset listing: papers issued by MagnetoCorp. + */ + async queryPartial(ctx, prefix) { + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let partial_results = await query.queryKeyByPartial(prefix); + + return partial_results; + } + + /** + * queryAdHoc commercial paper - supply a custom mango query + * eg - as supplied as a param: + * ex1: ["{\"selector\":{\"faceValue\":{\"$lt\":8000000}}}"] + * ex2: ["{\"selector\":{\"faceValue\":{\"$gt\":4999999}}}"] + * + * @param {Context} ctx the transaction context + * @param {String} queryString querystring + */ + async queryAdhoc(ctx, queryString) { + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let querySelector = JSON.parse(queryString); + let adhoc_results = await query.queryByAdhoc(querySelector); + + return adhoc_results; + } + + + /** + * queryNamed - supply named query - 'case' statement chooses selector to build (pre-canned for demo purposes) + * @param {Context} ctx the transaction context + * @param {String} queryname the 'named' query (built here) - or - the adHoc query string, provided as a parameter + */ + async queryNamed(ctx, queryname) { + let querySelector = {}; + switch (queryname) { + case "redeemed": + querySelector = { "selector": { "currentState": 4 } }; // 4 = redeemd state + break; + case "trading": + querySelector = { "selector": { "currentState": 3 } }; // 3 = trading state + break; + case "value": + // may change to provide as a param - fixed value for now in this sample + querySelector = { "selector": { "faceValue": { "$gt": 4000000 } } }; // to test, issue CommPapers with faceValue <= or => this figure. + break; + default: // else, unknown named query + throw new Error('invalid named query supplied: ' + queryname + '- please try again '); + } + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let adhoc_results = await query.queryByAdhoc(querySelector); + + return adhoc_results; + } + +} + +module.exports = CommercialPaperContract; diff --git a/commercial-paper/organization/digibank/contract/lib/paperlist.js b/commercial-paper/organization/digibank/contract/lib/paperlist.js new file mode 100644 index 0000000..73528b6 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/lib/paperlist.js @@ -0,0 +1,35 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Utility class for collections of ledger states -- a state list +const StateList = require('./../ledger-api/statelist.js'); + +const CommercialPaper = require('./paper.js'); + +class PaperList extends StateList { + + constructor(ctx) { + super(ctx, 'org.papernet.paper'); + this.use(CommercialPaper); + } + + async addPaper(paper) { + return this.addState(paper); + } + + async getPaper(paperKey) { + return this.getState(paperKey); + } + + async updatePaper(paper) { + return this.updateState(paper); + } +} + + +module.exports = PaperList; diff --git a/commercial-paper/organization/digibank/contract/lib/queries.js b/commercial-paper/organization/digibank/contract/lib/queries.js new file mode 100644 index 0000000..a7cd40a --- /dev/null +++ b/commercial-paper/organization/digibank/contract/lib/queries.js @@ -0,0 +1,215 @@ + +/* +SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const State = require('../ledger-api/state.js'); +//const CommercialPaper = require('./paper.js'); +/** + * Query Class for query functions such as history etc + * + */ +class QueryUtils { + + constructor(ctx, listName) { + this.ctx = ctx; + this.name = listName; + //this.supportedTypes = {}; + } + + // ========================================================================================= + // getAssetHistory takes the composite key as arg, gets returns results as JSON to 'main contract' + // ========================================================================================= + /** + * Get Asset History for a commercial paper + * @param {String} issuer the CP issuer + * @param {String} paperNumber commercial paper number + */ + async getAssetHistory(issuer, paperNumber) { + + let ledgerKey = await this.ctx.stub.createCompositeKey(this.name, [issuer, paperNumber]); + const resultsIterator = await this.ctx.stub.getHistoryForKey(ledgerKey); + let results = await this.getAllResults(resultsIterator, true); + + return results; + } + + // =========================================================================================== + // queryKeyByPartial performs a partial query based on the namespace and asset key prefix provided + + // Read-only function results are not typically submitted to ordering. If the read-only + // results are submitted to ordering, or if the query is used in an update transaction + // and submitted to ordering, then the committing peers will re-execute to guarantee that + // result sets are stable between endorsement time and commit time. The transaction is + // invalidated by the committing peers if the result set has changed between endorsement + // time and commit time. + // + // =========================================================================================== + /** + * queryOwner commercial paper + * @param {String} assetspace the asset space (eg MagnetoCorp's assets) + */ + async queryKeyByPartial(assetspace) { + + if (arguments.length < 1) { + throw new Error('Incorrect number of arguments. Expecting 1'); + } + // ie namespace + prefix to assets etc eg + // "Key":"org.papernet.paperMagnetoCorp0001" (0002, etc) + // "Partial":'org.papernet.paperlistMagnetoCorp"' (using partial key, find keys "0001", "0002" etc) + const resultsIterator = await this.ctx.stub.getStateByPartialCompositeKey(this.name, [assetspace]); + let method = this.getAllResults; + let results = await method(resultsIterator, false); + + return results; + } + + + // ===== Example: Parameterized rich query ================================================= + // queryKeyByOwner queries for assets based on a passed in owner. + // This is an example of a parameterized query accepting a single query parameter (owner). + // Only available on state databases that support rich query (e.g. CouchDB) + // ========================================================================================= + /** + * queryKeyByOwner commercial paper + * @param {String} owner commercial paper owner + */ + async queryKeyByOwner(owner) { + // + let self = this; + if (arguments.length < 1) { + throw new Error('Incorrect number of arguments. Expecting owner name.'); + } + let queryString = {}; + queryString.selector = {}; + // queryString.selector.docType = 'indexOwnerDoc'; + queryString.selector.owner = owner; + // set to (eg) '{selector:{owner:MagnetoCorp}}' + let method = self.getQueryResultForQueryString; + let queryResults = await method(this.ctx, self, JSON.stringify(queryString)); + return queryResults; + } + + // ===== Example: Ad hoc rich query ======================================================== + // queryAdhoc uses a query string to perform a query for marbles.. + // Query string matching state database syntax is passed in and executed as is. + // Supports ad hoc queries that can be defined at runtime by the client. + // If this is not desired, follow the queryKeyByOwner example for parameterized queries. + // Only available on state databases that support rich query (e.g. CouchDB) + // example passed using VS Code ext: ["{\"selector\": {\"owner\": \"MagnetoCorp\"}}"] + // ========================================================================================= + /** + * query By AdHoc string (commercial paper) + * @param {String} queryString actual MangoDB query string (escaped) + */ + async queryByAdhoc(queryString) { + + if (arguments.length < 1) { + throw new Error('Incorrect number of arguments. Expecting ad-hoc string, which gets stringified for mango query'); + } + let self = this; + + if (!queryString) { + throw new Error('queryString must not be empty'); + } + let method = self.getQueryResultForQueryString; + let queryResults = await method(this.ctx, self, JSON.stringify(queryString)); + return queryResults; + } + + // WORKER functions are below this line: these are called by the above functions, where iterator is passed in + + // ========================================================================================= + // getQueryResultForQueryString woerk function executes the passed-in query string. + // Result set is built and returned as a byte array containing the JSON results. + // ========================================================================================= + /** + * Function getQueryResultForQueryString + * @param {Context} ctx the transaction context + * @param {any} self within scope passed in + * @param {String} the query string created prior to calling this fn + */ + async getQueryResultForQueryString(ctx, self, queryString) { + + // console.log('- getQueryResultForQueryString queryString:\n' + queryString); + + const resultsIterator = await ctx.stub.getQueryResult(queryString); + let results = await self.getAllResults(resultsIterator, false); + + return results; + + } + + /** + * Function getAllResults + * @param {resultsIterator} iterator within scope passed in + * @param {Boolean} isHistory query string created prior to calling this fn + */ + async getAllResults(iterator, isHistory) { + let allResults = []; + let res = { done: false, value: null }; + + while (true) { + res = await iterator.next(); + let jsonRes = {}; + if (res.value && res.value.value.toString()) { + if (isHistory && isHistory === true) { + //jsonRes.TxId = res.value.tx_id; + jsonRes.TxId = res.value.txId; + jsonRes.Timestamp = res.value.timestamp; + jsonRes.Timestamp = new Date((res.value.timestamp.seconds.low * 1000)); + let ms = res.value.timestamp.nanos / 1000000; + jsonRes.Timestamp.setMilliseconds(ms); + if (res.value.is_delete) { + jsonRes.IsDelete = res.value.is_delete.toString(); + } else { + try { + jsonRes.Value = JSON.parse(res.value.value.toString('utf8')); + // report the commercial paper states during the asset lifecycle, just for asset history reporting + switch (jsonRes.Value.currentState) { + case 1: + jsonRes.Value.currentState = 'ISSUED'; + break; + case 2: + jsonRes.Value.currentState = 'PENDING'; + break; + case 3: + jsonRes.Value.currentState = 'TRADING'; + break; + case 4: + jsonRes.Value.currentState = 'REDEEMED'; + break; + default: // else, unknown named query + jsonRes.Value.currentState = 'UNKNOWN'; + } + + } catch (err) { + console.log(err); + jsonRes.Value = res.value.value.toString('utf8'); + } + } + } else { // non history query .. + jsonRes.Key = res.value.key; + try { + jsonRes.Record = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Record = res.value.value.toString('utf8'); + } + } + allResults.push(jsonRes); + } + // check to see if we have reached the end + if (res.done) { + // explicitly close the iterator + console.log('iterator is done'); + await iterator.close(); + return allResults; + } + + } // while true + } + +} +module.exports = QueryUtils; \ No newline at end of file diff --git a/commercial-paper/organization/digibank/contract/package.json b/commercial-paper/organization/digibank/contract/package.json new file mode 100644 index 0000000..2c4a644 --- /dev/null +++ b/commercial-paper/organization/digibank/contract/package.json @@ -0,0 +1,49 @@ +{ + "name": "papernet-js", + "version": "0.0.1", + "description": "Papernet Contract", + "main": "index.js", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha test --recursive", + "start": "fabric-chaincode-node start", + "mocha": "mocha test --recursive" + }, + "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": "^5.2.0", + "nyc": "^12.0.2", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/commercial-paper/organization/digibank/contract/test/contract.js b/commercial-paper/organization/digibank/contract/test/contract.js new file mode 100644 index 0000000..63576ee --- /dev/null +++ b/commercial-paper/organization/digibank/contract/test/contract.js @@ -0,0 +1,43 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const Chaincode = require('../lib/chaincode'); +const { Stub } = require('fabric-shim'); + +require('chai').should(); +const sinon = require('sinon'); + +describe('Chaincode', () => { + + describe('#Init', () => { + + it('should work', async () => { + const cc = new Chaincode(); + const stub = sinon.createStubInstance(Stub); + stub.getFunctionAndParameters.returns({ fcn: 'initFunc', params: [] }); + const res = await cc.Init(stub); + res.status.should.equal(Stub.RESPONSE_CODE.OK); + }); + + }); + + describe('#Invoke', async () => { + + it('should work', async () => { + const cc = new Chaincode(); + const stub = sinon.createStubInstance(Stub); + stub.getFunctionAndParameters.returns({ fcn: 'initFunc', params: [] }); + let res = await cc.Init(stub); + res.status.should.equal(Stub.RESPONSE_CODE.OK); + stub.getFunctionAndParameters.returns({ fcn: 'invokeFunc', params: [] }); + res = await cc.Invoke(stub); + res.status.should.equal(Stub.RESPONSE_CODE.OK); + }); + + }); + +}); diff --git a/commercial-paper/organization/digibank/digibank.sh b/commercial-paper/organization/digibank/digibank.sh new file mode 100755 index 0000000..414d4d2 --- /dev/null +++ b/commercial-paper/organization/digibank/digibank.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# SPDX-License-Identifier: Apache-2.0 +# +shopt -s extglob + +function _exit(){ + printf "Exiting:%s\n" "$1" + exit -1 +} + +: ${CHANNEL_NAME:="mychannel"} +: ${DELAY:="3"} +: ${MAX_RETRY:="5"} +: ${VERBOSE:="false"} + +# Where am I? +DIR=${PWD} + +# Locate the test network +cd "${DIR}/../../../test-network" +env | sort > /tmp/env.orig + +OVERRIDE_ORG="1" +. ./scripts/envVar.sh + +parsePeerConnectionParameters 1 2 + +export PEER_PARMS="${PEER_CONN_PARMS##*( )}" + +# set the fabric config path +export FABRIC_CFG_PATH="${DIR}/../../../config" +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} diff --git a/commercial-paper/organization/digibank/gateway/.gitkeep b/commercial-paper/organization/digibank/gateway/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/commercial-paper/organization/magnetocorp/.gitignore b/commercial-paper/organization/magnetocorp/.gitignore new file mode 100644 index 0000000..7d13901 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/.gitignore @@ -0,0 +1 @@ +identity \ No newline at end of file diff --git a/commercial-paper/organization/magnetocorp/application-java/.gitignore b/commercial-paper/organization/magnetocorp/application-java/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.core.resources.prefs b/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..7a53139 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 +encoding/src=UTF-8 diff --git a/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.jdt.core.prefs b/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b8947ec --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.m2e.core.prefs b/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/commercial-paper/organization/magnetocorp/application-java/pom.xml b/commercial-paper/organization/magnetocorp/application-java/pom.xml new file mode 100644 index 0000000..f80dc68 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/pom.xml @@ -0,0 +1,96 @@ + + 4.0.0 + commercial-paper + commercial-paper + 0.0.1-SNAPSHOT + + + + + 1.8 + UTF-8 + UTF-8 + + + [2.0,3.0) + + 2.1.0 + + + + + src + + + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.0 + + + + package + + shade + + + + + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + jitpack.io + https://jitpack.io + + + + + + org.hyperledger.fabric + fabric-gateway-java + ${fabric-gateway-java.version} + + + + + org.hyperledger.fabric-chaincode-java + fabric-chaincode-shim + ${fabric-chaincode-java.version} + compile + + + + + org.json + json + 20180813 + + + + diff --git a/commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/AddToWallet.java b/commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/AddToWallet.java new file mode 100644 index 0000000..36d0c1f --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/AddToWallet.java @@ -0,0 +1,68 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.magnetocorp; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.hyperledger.fabric.gateway.Identity; + +public class AddToWallet { + + private static X509Certificate readX509Certificate(final Path certificatePath) throws IOException, CertificateException { + try (Reader certificateReader = Files.newBufferedReader(certificatePath, StandardCharsets.UTF_8)) { + return Identities.readX509Certificate(certificateReader); + } + } + + private static PrivateKey getPrivateKey(final Path privateKeyPath) throws IOException, InvalidKeyException { + try (Reader privateKeyReader = Files.newBufferedReader(privateKeyPath, StandardCharsets.UTF_8)) { + return Identities.readPrivateKey(privateKeyReader); + } + } + + public static void main(String[] args) { + try { + // A wallet stores a collection of identities + Path walletPath = Paths.get(".", "wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + + Path credentialPath = Paths.get("..", "..", "..",".." ,"test-network", "organizations", + "peerOrganizations", "org2.example.com", "users", "User1@org2.example.com", "msp"); + System.out.println("credentialPath: " + credentialPath.toString()); + Path certificatePath = credentialPath.resolve(Paths.get("signcerts", + "User1@org2.example.com-cert.pem")); + System.out.println("certificatePem: " + certificatePath.toString()); + Path privateKeyPath = credentialPath.resolve(Paths.get("keystore", + "priv_sk")); + + X509Certificate certificate = readX509Certificate(certificatePath); + PrivateKey privateKey = getPrivateKey(privateKeyPath); + + Identity identity = Identities.newX509Identity("Org2MSP", certificate, privateKey); + + + String identityLabel = "User1@org2.example.com"; + wallet.put(identityLabel, identity); + + System.out.println("Write wallet info into " + walletPath.toString() + " successfully."); + + } catch (IOException | CertificateException | InvalidKeyException e) { + System.err.println("Error adding to wallet"); + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/Issue.java b/commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/Issue.java new file mode 100644 index 0000000..cdc268e --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/src/org/magnetocorp/Issue.java @@ -0,0 +1,75 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.magnetocorp; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +import org.hyperledger.fabric.gateway.Contract; +import org.hyperledger.fabric.gateway.Gateway; +import org.hyperledger.fabric.gateway.GatewayException; +import org.hyperledger.fabric.gateway.Network; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.papernet.CommercialPaper; + +public class Issue { + + private static final String ENVKEY="CONTRACT_NAME"; + + public static void main(String[] args) { + + String contractName="papercontract"; + // get the name of the contract, in case it is overridden + Map envvar = System.getenv(); + if (envvar.containsKey(ENVKEY)){ + contractName=envvar.get(ENVKEY); + } + + Gateway.Builder builder = Gateway.createBuilder(); + + try { + // A wallet stores a collection of identities + Path walletPath = Paths.get(".", "wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + System.out.println("Read wallet info from: " + walletPath.toString()); + + String userName = "User1@org2.example.com"; + + Path connectionProfile = Paths.get("..", "gateway", "connection-org2.yaml"); + + // Set connection options on the gateway builder + builder.identity(wallet, userName).networkConfig(connectionProfile).discovery(false); + + // Connect to gateway using application specified parameters + try(Gateway gateway = builder.connect()) { + + // Access PaperNet network + System.out.println("Use network channel: mychannel."); + Network network = gateway.getNetwork("mychannel"); + + // Get addressability to commercial paper contract + System.out.println("Use org.papernet.commercialpaper smart contract."); + Contract contract = network.getContract(contractName, "org.papernet.commercialpaper"); + + // Issue commercial paper + System.out.println("Submit commercial paper issue transaction."); + byte[] response = contract.submitTransaction("issue", "MagnetoCorp", "00001", "2020-05-31", "2020-11-30", "5000000"); + + // Process response + System.out.println("Process issue transaction response."); + CommercialPaper paper = CommercialPaper.deserialize(response); + System.out.println(paper); + } + } catch (GatewayException | IOException | TimeoutException | InterruptedException e) { + e.printStackTrace(); + System.exit(-1); + } + } + +} \ No newline at end of file diff --git a/commercial-paper/organization/magnetocorp/application-java/src/org/papernet/CommercialPaper.java b/commercial-paper/organization/magnetocorp/application-java/src/org/papernet/CommercialPaper.java new file mode 100644 index 0000000..e909b49 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/src/org/papernet/CommercialPaper.java @@ -0,0 +1,183 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.papernet; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; +import org.json.JSONObject; +import org.json.JSONPropertyIgnore; +import org.papernet.ledgerapi.State; + +@DataType() +public class CommercialPaper extends State { + + // Enumerate commercial paper state values + public final static String ISSUED = "ISSUED"; + public final static String TRADING = "TRADING"; + public final static String REDEEMED = "REDEEMED"; + + @Property() + private String state=""; + + public String getState() { + return state; + } + + public CommercialPaper setState(String state) { + this.state = state; + return this; + } + + @JSONPropertyIgnore() + public boolean isIssued() { + return this.state.equals(CommercialPaper.ISSUED); + } + + @JSONPropertyIgnore() + public boolean isTrading() { + return this.state.equals(CommercialPaper.TRADING); + } + + @JSONPropertyIgnore() + public boolean isRedeemed() { + return this.state.equals(CommercialPaper.REDEEMED); + } + + public CommercialPaper setIssued() { + this.state = CommercialPaper.ISSUED; + return this; + } + + public CommercialPaper setTrading() { + this.state = CommercialPaper.TRADING; + return this; + } + + public CommercialPaper setRedeemed() { + this.state = CommercialPaper.REDEEMED; + return this; + } + + @Property() + private String paperNumber; + + @Property() + private String issuer; + + @Property() + private String issueDateTime; + + @Property() + private int faceValue; + + @Property() + private String maturityDateTime; + + @Property() + private String owner; + + public String getOwner() { + return owner; + } + + public CommercialPaper setOwner(String owner) { + this.owner = owner; + return this; + } + + public CommercialPaper() { + super(); + } + + public CommercialPaper setKey() { + this.key = State.makeKey(new String[] { this.paperNumber }); + return this; + } + + public String getPaperNumber() { + return paperNumber; + } + + public CommercialPaper setPaperNumber(String paperNumber) { + this.paperNumber = paperNumber; + return this; + } + + public String getIssuer() { + return issuer; + } + + public CommercialPaper setIssuer(String issuer) { + this.issuer = issuer; + return this; + } + + public String getIssueDateTime() { + return issueDateTime; + } + + public CommercialPaper setIssueDateTime(String issueDateTime) { + this.issueDateTime = issueDateTime; + return this; + } + + public int getFaceValue() { + return faceValue; + } + + public CommercialPaper setFaceValue(int faceValue) { + this.faceValue = faceValue; + return this; + } + + public String getMaturityDateTime() { + return maturityDateTime; + } + + public CommercialPaper setMaturityDateTime(String maturityDateTime) { + this.maturityDateTime = maturityDateTime; + return this; + } + + @Override + public String toString() { + return "Paper::" + this.key + " " + this.getPaperNumber() + " " + getIssuer() + " " + getFaceValue(); + } + + /** + * Deserialize a state data to commercial paper + * + * @param {Buffer} data to form back into the object + */ + public static CommercialPaper deserialize(byte[] data) { + JSONObject json = new JSONObject(new String(data, UTF_8)); + + String issuer = json.getString("issuer"); + String paperNumber = json.getString("paperNumber"); + String issueDateTime = json.getString("issueDateTime"); + String maturityDateTime = json.getString("maturityDateTime"); + String owner = json.getString("owner"); + int faceValue = json.getInt("faceValue"); + String state = json.getString("state"); + return createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue, owner, state); + } + + public static byte[] serialize(CommercialPaper paper) { + return State.serialize(paper); + } + + /** + * Factory method to create a commercial paper object + */ + public static CommercialPaper createInstance(String issuer, String paperNumber, String issueDateTime, + String maturityDateTime, int faceValue, String owner, String state) { + return new CommercialPaper().setIssuer(issuer).setPaperNumber(paperNumber).setMaturityDateTime(maturityDateTime) + .setFaceValue(faceValue).setKey().setIssueDateTime(issueDateTime).setOwner(issuer).setState(state); + } + + +} diff --git a/commercial-paper/organization/magnetocorp/application-java/src/org/papernet/ledgerapi/State.java b/commercial-paper/organization/magnetocorp/application-java/src/org/papernet/ledgerapi/State.java new file mode 100644 index 0000000..a32abc0 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application-java/src/org/papernet/ledgerapi/State.java @@ -0,0 +1,60 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ +package org.papernet.ledgerapi; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.json.JSONObject; + +/** + * State class. States have a class, unique key, and a lifecycle current state + * the current state is determined by the specific subclass + */ +public class State { + + protected String key; + + /** + * @param {String|Object} class An identifiable class of the instance + * @param {keyParts[]} elements to pull together to make a key for the objects + */ + public State() { + + } + + String getKey() { + return this.key; + } + + public String[] getSplitKey() { + return State.splitKey(this.key); + } + + /** + * Convert object to buffer containing JSON data serialization Typically used + * before putState()ledger API + * + * @param {Object} JSON object to serialize + * @return {buffer} buffer with the data to store + */ + public static byte[] serialize(Object object) { + String jsonStr = new JSONObject(object).toString(); + return jsonStr.getBytes(UTF_8); + } + + /** + * Join the keyParts to make a unififed string + * + * @param (String[]) keyParts + */ + public static String makeKey(String[] keyParts) { + return String.join(":", keyParts); + } + + public static String[] splitKey(String key) { + System.out.println("splitting key " + key + " " + java.util.Arrays.asList(key.split(":"))); + return key.split(":"); + } + +} diff --git a/commercial-paper/organization/magnetocorp/application/.eslintrc.js b/commercial-paper/organization/magnetocorp/application/.eslintrc.js new file mode 100644 index 0000000..22fbefc --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/.eslintrc.js @@ -0,0 +1,37 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +module.exports = { + env: { + node: 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 newline at end of file diff --git a/commercial-paper/organization/magnetocorp/application/.gitignore b/commercial-paper/organization/magnetocorp/application/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/commercial-paper/organization/magnetocorp/application/addToWallet.js b/commercial-paper/organization/magnetocorp/application/addToWallet.js new file mode 100644 index 0000000..eb49123 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/addToWallet.js @@ -0,0 +1,55 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const { Wallets } = require('fabric-network'); +const path = require('path'); + +const fixtures = path.resolve(__dirname, '../../../../test-network'); + +async function main() { + + // Main try/catch block + try { + // A wallet stores a collection of identities + const wallet = await Wallets.newFileSystemWallet('../identity/user/isabella/wallet'); + + // Identity to credentials to be stored in the wallet + const credPath = path.join(fixtures, '/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com'); + const certificate = fs.readFileSync(path.join(credPath, '/msp/signcerts/User1@org2.example.com-cert.pem')).toString(); + const privateKey = fs.readFileSync(path.join(credPath, '/msp/keystore/priv_sk')).toString(); + + // Load credentials into wallet + const identityLabel = 'isabella'; + + const identity = { + credentials: { + certificate, + privateKey + }, + mspId: 'Org2MSP', + type: 'X.509' + } + + + await wallet.put(identityLabel,identity); + + } catch (error) { + console.log(`Error adding to wallet. ${error}`); + console.log(error.stack); + } +} + +main().then(() => { + console.log('done'); +}).catch((e) => { + console.log(e); + console.log(e.stack); + process.exit(-1); +}); diff --git a/commercial-paper/organization/magnetocorp/application/cpListener.js b/commercial-paper/organization/magnetocorp/application/cpListener.js new file mode 100644 index 0000000..78c8ba1 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/cpListener.js @@ -0,0 +1,97 @@ +"use strict"; + +const yaml = require('js-yaml'); +const { Wallets, Gateway } = require('fabric-network'); +const path = require("path"); +const fs = require("fs"); + +let finished; +async function main() { + try { + // Set up the wallet - just use Org2's wallet (isabella) + const wallet = await Wallets.newFileSystemWallet('../identity/user/isabella/wallet'); + + // Create a new gateway for connecting to our peer node. + const gateway = new Gateway(); + + const userName = 'isabella'; + + // Load connection profile; will be used to locate a gateway + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org2.yaml', 'utf8')); + + // Set connection options; identity and wallet + let connectionOptions = { + identity: userName, + wallet: wallet, + discovery: { enabled:true, asLocalhost: true } + }; + + // connect to the gateway + await gateway.connect(connectionProfile, connectionOptions); + // get the channel and smart contract + const network = await gateway.getNetwork('mychannel'); + + // Listen for blocks being added, display relevant contents: in particular, the transaction inputs + finished = false; + + const listener = async (event) => { + if (event.blockData !== undefined) { + for (const i in event.blockData.data.data) { + if (event.blockData.data.data[i].payload.data.actions !== undefined) { + const inputArgs = event.blockData.data.data[i].payload.data.actions[0].payload.chaincode_proposal_payload.input.chaincode_spec.input.args; + // Print block details + console.log('----------'); + console.log('Block:', parseInt(event.blockData.header.number), 'transaction', i); + // Show ID and timestamp of the transaction + const tx_id = event.blockData.data.data[i].payload.header.channel_header.tx_id; + const txTime = new Date(event.blockData.data.data[i].payload.header.channel_header.timestamp).toUTCString(); + // Show ID, date and time of transaction + console.log('Transaction ID:', tx_id); + console.log('Timestamp:', txTime); + // Show transaction inputs (formatted, as may contain binary data) + let inputData = 'Inputs: '; + for (let j = 0; j < inputArgs.length; j++) { + const inputArgPrintable = inputArgs[j].toString().replace(/[^\x20-\x7E]+/g, ''); + inputData = inputData.concat(inputArgPrintable, ' '); + } + console.log(inputData); + // Show the proposed writes to the world state + let keyData = 'Keys updated: '; + for (const l in event.blockData.data.data[i].payload.data.actions[0].payload.action.proposal_response_payload.extension.results.ns_rwset[1].rwset.writes) { + // add a ' ' space between multiple keys in 'concat' + keyData = keyData.concat(event.blockData.data.data[i].payload.data.actions[0].payload.action.proposal_response_payload.extension.results.ns_rwset[1].rwset.writes[l].key, ' '); + } + console.log(keyData); + // Show which organizations endorsed + let endorsers = 'Endorsers: '; + for (const k in event.blockData.data.data[i].payload.data.actions[0].payload.action.endorsements) { + endorsers = endorsers.concat(event.blockData.data.data[i].payload.data.actions[0].payload.action.endorsements[k].endorser.mspid, ' '); + } + console.log(endorsers); + // Was the transaction valid or not? + // (Invalid transactions are still logged on the blockchain but don't affect the world state) + if ((event.blockData.metadata.metadata[2])[i] !== 0) { + console.log('INVALID TRANSACTION'); + } + } + } + } + }; + const options = { + type: 'full', + startBlock: 1 + }; + await network.addBlockListener(listener, options); + while (!finished) { + await new Promise(resolve => setTimeout(resolve, 5000)); + // Disconnect from the gateway after Promise is resolved. + // ... do other things + } + gateway.disconnect(); + } + catch (error) { + console.error('Error: ', error); + process.exit(1); + } +} +void main(); diff --git a/commercial-paper/organization/magnetocorp/application/enrollUser.js b/commercial-paper/organization/magnetocorp/application/enrollUser.js new file mode 100644 index 0000000..bb673f0 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/enrollUser.js @@ -0,0 +1,56 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const FabricCAServices = require('fabric-ca-client'); +const { Wallets } = require('fabric-network'); +const fs = require('fs'); +const yaml = require('js-yaml'); +const path = require('path'); + +async function main() { + try { + // load the network configuration + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org2.yaml', 'utf8')); + + // Create a new CA client for interacting with the CA. + const caInfo = connectionProfile.certificateAuthorities['ca.org2.example.com']; + const caTLSCACerts = caInfo.tlsCACerts.pem; + const ca = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), '../identity/user/isabella/wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the admin user. + const userExists = await wallet.get('isabella'); + if (userExists) { + console.log('An identity for the client user "user1" already exists in the wallet'); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + const enrollment = await ca.enroll({ enrollmentID: 'user1', enrollmentSecret: 'user1pw' }); + const x509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org2MSP', + type: 'X.509', + }; + await wallet.put('isabella', x509Identity); + console.log('Successfully enrolled client user "isabella" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to enroll client user "isabella": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/commercial-paper/organization/magnetocorp/application/issue.js b/commercial-paper/organization/magnetocorp/application/issue.js new file mode 100644 index 0000000..2d44fce --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/issue.js @@ -0,0 +1,103 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +/* + * This application has 6 basic steps: + * 1. Select an identity from a wallet + * 2. Connect to network gateway + * 3. Access PaperNet network + * 4. Construct request to issue commercial paper + * 5. Submit transaction + * 6. Process response + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const yaml = require('js-yaml'); +const { Wallets, Gateway } = require('fabric-network'); +const CommercialPaper = require('../contract/lib/paper.js'); + +// Main program function +async function main() { + + // A wallet stores a collection of identities for use + const wallet = await Wallets.newFileSystemWallet('../identity/user/isabella/wallet'); + + // A gateway defines the peers used to access Fabric networks + const gateway = new Gateway(); + + // Main try/catch block + try { + + // Specify userName for network access + // const userName = 'isabella.issuer@magnetocorp.com'; + const userName = 'isabella'; + + // Load connection profile; will be used to locate a gateway + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org2.yaml', 'utf8')); + + // Set connection options; identity and wallet + let connectionOptions = { + identity: userName, + wallet: wallet, + discovery: { enabled:true, asLocalhost: true } + }; + + // Connect to gateway using application specified parameters + console.log('Connect to Fabric gateway.'); + + await gateway.connect(connectionProfile, connectionOptions); + + // Access PaperNet network + console.log('Use network channel: mychannel.'); + + const network = await gateway.getNetwork('mychannel'); + + // Get addressability to commercial paper contract + console.log('Use org.papernet.commercialpaper smart contract.'); + + const contract = await network.getContract('papercontract'); + + // issue commercial paper + console.log('Submit commercial paper issue transaction.'); + + const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000'); + + // process response + console.log('Process issue transaction response.'+issueResponse); + + let paper = CommercialPaper.fromBuffer(issueResponse); + + console.log(`${paper.issuer} commercial paper : ${paper.paperNumber} successfully issued for value ${paper.faceValue}`); + console.log('Transaction complete.'); + + } catch (error) { + + console.log(`Error processing transaction. ${error}`); + console.log(error.stack); + + } finally { + + // Disconnect from the gateway + console.log('Disconnect from Fabric gateway.'); + gateway.disconnect(); + + } +} +main().then(() => { + + console.log('Issue program complete.'); + +}).catch((e) => { + + console.log('Issue program exception.'); + console.log(e); + console.log(e.stack); + process.exit(-1); + +}); diff --git a/commercial-paper/organization/magnetocorp/application/package.json b/commercial-paper/organization/magnetocorp/application/package.json new file mode 100644 index 0000000..f8fac56 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs", + "version": "1.0.0", + "description": "", + "main": "issue.js", + "scripts": { + "test": "rm -rf _idwallet && node addToWallet.js && node issue.js" + }, + "keywords": [], + "author": "", + "license": "Apache-2.0", + "dependencies": { + "fabric-network": "^2.2.4", + "fabric-ca-client": "^2.2.4", + "js-yaml": "^3.12.0" + }, + "devDependencies": { + "eslint": "^5.6.0" + } +} diff --git a/commercial-paper/organization/magnetocorp/application/transfer.js b/commercial-paper/organization/magnetocorp/application/transfer.js new file mode 100644 index 0000000..908903a --- /dev/null +++ b/commercial-paper/organization/magnetocorp/application/transfer.js @@ -0,0 +1,103 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +/* + * This application has 6 basic steps: + * 1. Select an identity from a wallet + * 2. Connect to network gateway + * 3. Access PaperNet network + * 4. Construct request to transfer commercial paper + * 5. Submit transaction + * 6. Process response + */ + +'use strict'; + +// Bring key classes into scope, most importantly Fabric SDK network class +const fs = require('fs'); +const yaml = require('js-yaml'); +const { Wallets, Gateway } = require('fabric-network'); +const CommercialPaper = require('../contract/lib/paper.js'); + +// Main program function +async function main() { + + // A wallet stores a collection of identities for use + const wallet = await Wallets.newFileSystemWallet('../identity/user/isabella/wallet'); + + // A gateway defines the peers used to access Fabric networks + const gateway = new Gateway(); + + // Main try/catch block + try { + + // Specify userName for network access + // const userName = 'isabella.issuer@magnetocorp.com'; + const userName = 'isabella'; + + // Load connection profile; will be used to locate a gateway + let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/connection-org2.yaml', 'utf8')); + + // Set connection options; identity and wallet + let connectionOptions = { + identity: userName, + wallet: wallet, + discovery: { enabled:true, asLocalhost: true } + }; + + // Connect to gateway using application specified parameters + console.log('Connect to Fabric gateway.'); + + await gateway.connect(connectionProfile, connectionOptions); + + // Access PaperNet network + console.log('Use network channel: mychannel.'); + + const network = await gateway.getNetwork('mychannel'); + + // Get addressability to commercial paper contract + console.log('Use org.papernet.commercialpaper smart contract.'); + + const contract = await network.getContract('papercontract'); + + // transfer commercial paper + console.log('Submit commercial paper transfer transaction.'); + + const transferResponse = await contract.submitTransaction('transfer', 'MagnetoCorp', '00001', 'DigiBank', 'Org1MSP', '2020-06-01'); + + // process response + console.log('Process transfer transaction response.'+ transferResponse); + + let paper = CommercialPaper.fromBuffer(transferResponse); + + console.log(`commercial paper issued by ${paper.issuer} : ${paper.paperNumber} was successfully transferred`); + console.log('Transaction complete.'); + + } catch (error) { + + console.log(`Error processing transaction. ${error}`); + console.log(error.stack); + + } finally { + + // Disconnect from the gateway + console.log('Disconnect from Fabric gateway.'); + gateway.disconnect(); + + } +} +main().then(() => { + + console.log('Transfer program complete.'); + +}).catch((e) => { + + console.log('Transfer program exception.'); + console.log(e); + console.log(e.stack); + process.exit(-1); + +}); diff --git a/commercial-paper/organization/magnetocorp/configuration/cli/docker-compose.yml b/commercial-paper/organization/magnetocorp/configuration/cli/docker-compose.yml new file mode 100644 index 0000000..3bdda81 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/configuration/cli/docker-compose.yml @@ -0,0 +1,37 @@ +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +version: '2' + +networks: + basic: + external: + name: net_test + +services: + cliMagnetoCorp: + container_name: cliMagnetoCorp + image: hyperledger/fabric-tools:2.0.0-beta + tty: true + environment: + - GOPATH=/opt/gopath + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + - FABRIC_LOGGING_SPEC=info + - CORE_PEER_ID=cli + - CORE_PEER_ADDRESS=peer0.org2.example.com:9051 + - CORE_PEER_LOCALMSPID=Org2MSP + - CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp + - CORE_CHAINCODE_KEEPALIVE=10 + - CORE_PEER_TLS_ENABLED=true + - CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt + - ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: /bin/bash + volumes: + - /var/run/docker.sock:/host/var/run/docker.sock + - ./../../../../organization/magnetocorp:/opt/gopath/src/github.com/ + - ./../../../../../test-network/organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ + networks: + - basic diff --git a/commercial-paper/organization/magnetocorp/configuration/cli/monitordocker.sh b/commercial-paper/organization/magnetocorp/configuration/cli/monitordocker.sh new file mode 100755 index 0000000..cd388b2 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/configuration/cli/monitordocker.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# This script uses the logspout and http stream tools to let you watch the docker containers +# in action. +# +# More information at https://github.com/gliderlabs/logspout/tree/master/httpstream + +if [ -z "$1" ]; then + DOCKER_NETWORK=net_test +else + DOCKER_NETWORK="$1" +fi + +if [ -z "$2" ]; then + PORT=8000 +else + PORT="$2" +fi + +echo Starting monitoring on all containers on the network ${DOCKER_NETWORK} + +docker kill logspout 2> /dev/null 1>&2 || true +docker rm logspout 2> /dev/null 1>&2 || true + +docker run -d --name="logspout" \ + --volume=/var/run/docker.sock:/var/run/docker.sock \ + --publish=127.0.0.1:${PORT}:80 \ + --network ${DOCKER_NETWORK} \ + gliderlabs/logspout +sleep 3 +curl http://127.0.0.1:${PORT}/logs diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper.go new file mode 100644 index 0000000..7eecdf4 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper.go @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "encoding/json" + "fmt" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/contract-go/ledger-api" +) + +// State enum for commercial paper state property +type State uint + +const ( + // ISSUED state for when a paper has been issued + ISSUED State = iota + 1 + // TRADING state for when a paper is trading + TRADING + // REDEEMED state for when a paper has been redeemed + REDEEMED +) + +func (state State) String() string { + names := []string{"ISSUED", "TRADING", "REDEEMED"} + + if state < ISSUED || state > REDEEMED { + return "UNKNOWN" + } + + return names[state-1] +} + +// CreateCommercialPaperKey creates a key for commercial papers +func CreateCommercialPaperKey(issuer string, paperNumber string) string { + return ledgerapi.MakeKey(issuer, paperNumber) +} + +// Used for managing the fact status is private but want it in world state +type commercialPaperAlias CommercialPaper +type jsonCommercialPaper struct { + *commercialPaperAlias + State State `json:"currentState"` + Class string `json:"class"` + Key string `json:"key"` +} + +// CommercialPaper defines a commercial paper +type CommercialPaper struct { + PaperNumber string `json:"paperNumber"` + Issuer string `json:"issuer"` + IssueDateTime string `json:"issueDateTime"` + FaceValue int `json:"faceValue"` + MaturityDateTime string `json:"maturityDateTime"` + Owner string `json:"owner"` + state State `metadata:"currentState"` + class string `metadata:"class"` + key string `metadata:"key"` +} + +// UnmarshalJSON special handler for managing JSON marshalling +func (cp *CommercialPaper) UnmarshalJSON(data []byte) error { + jcp := jsonCommercialPaper{commercialPaperAlias: (*commercialPaperAlias)(cp)} + + err := json.Unmarshal(data, &jcp) + + if err != nil { + return err + } + + cp.state = jcp.State + + return nil +} + +// MarshalJSON special handler for managing JSON marshalling +func (cp CommercialPaper) MarshalJSON() ([]byte, error) { + jcp := jsonCommercialPaper{commercialPaperAlias: (*commercialPaperAlias)(&cp), State: cp.state, Class: "org.papernet.commercialpaper", Key: ledgerapi.MakeKey(cp.Issuer, cp.PaperNumber)} + + return json.Marshal(&jcp) +} + +// GetState returns the state +func (cp *CommercialPaper) GetState() State { + return cp.state +} + +// SetIssued returns the state to issued +func (cp *CommercialPaper) SetIssued() { + cp.state = ISSUED +} + +// SetTrading sets the state to trading +func (cp *CommercialPaper) SetTrading() { + cp.state = TRADING +} + +// SetRedeemed sets the state to redeemed +func (cp *CommercialPaper) SetRedeemed() { + cp.state = REDEEMED +} + +// IsIssued returns true if state is issued +func (cp *CommercialPaper) IsIssued() bool { + return cp.state == ISSUED +} + +// IsTrading returns true if state is trading +func (cp *CommercialPaper) IsTrading() bool { + return cp.state == TRADING +} + +// IsRedeemed returns true if state is redeemed +func (cp *CommercialPaper) IsRedeemed() bool { + return cp.state == REDEEMED +} + +// GetSplitKey returns values which should be used to form key +func (cp *CommercialPaper) GetSplitKey() []string { + return []string{cp.Issuer, cp.PaperNumber} +} + +// Serialize formats the commercial paper as JSON bytes +func (cp *CommercialPaper) Serialize() ([]byte, error) { + return json.Marshal(cp) +} + +// Deserialize formats the commercial paper from JSON bytes +func Deserialize(bytes []byte, cp *CommercialPaper) error { + err := json.Unmarshal(bytes, cp) + + if err != nil { + return fmt.Errorf("Error deserializing commercial paper. %s", err.Error()) + } + + return nil +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper_test.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper_test.go new file mode 100644 index 0000000..07c888b --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paper_test.go @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "testing" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/contract-go/ledger-api" + "github.com/stretchr/testify/assert" +) + +func TestString(t *testing.T) { + assert.Equal(t, "ISSUED", ISSUED.String(), "should return string for issued") + assert.Equal(t, "TRADING", TRADING.String(), "should return string for issued") + assert.Equal(t, "REDEEMED", REDEEMED.String(), "should return string for issued") + assert.Equal(t, "UNKNOWN", State(REDEEMED+1).String(), "should return unknown when not one of constants") +} + +func TestCreateCommercialPaperKey(t *testing.T) { + assert.Equal(t, ledgerapi.MakeKey("someissuer", "somepaper"), CreateCommercialPaperKey("someissuer", "somepaper"), "should return key comprised of passed values") +} + +func TestGetState(t *testing.T) { + cp := new(CommercialPaper) + cp.state = ISSUED + + assert.Equal(t, ISSUED, cp.GetState(), "should return set state") +} + +func TestSetIssued(t *testing.T) { + cp := new(CommercialPaper) + cp.SetIssued() + assert.Equal(t, ISSUED, cp.state, "should set state to trading") +} + +func TestSetTrading(t *testing.T) { + cp := new(CommercialPaper) + cp.SetTrading() + assert.Equal(t, TRADING, cp.state, "should set state to trading") +} + +func TestSetRedeemed(t *testing.T) { + cp := new(CommercialPaper) + cp.SetRedeemed() + assert.Equal(t, REDEEMED, cp.state, "should set state to trading") +} + +func TestIsIssued(t *testing.T) { + cp := new(CommercialPaper) + + cp.SetIssued() + assert.True(t, cp.IsIssued(), "should be true when status set to issued") + + cp.SetTrading() + assert.False(t, cp.IsIssued(), "should be false when status not set to issued") +} + +func TestIsTrading(t *testing.T) { + cp := new(CommercialPaper) + + cp.SetTrading() + assert.True(t, cp.IsTrading(), "should be true when status set to trading") + + cp.SetRedeemed() + assert.False(t, cp.IsTrading(), "should be false when status not set to trading") +} + +func TestIsRedeemed(t *testing.T) { + cp := new(CommercialPaper) + + cp.SetRedeemed() + assert.True(t, cp.IsRedeemed(), "should be true when status set to redeemed") + + cp.SetIssued() + assert.False(t, cp.IsRedeemed(), "should be false when status not set to redeemed") +} + +func TestGetSplitKey(t *testing.T) { + cp := new(CommercialPaper) + cp.PaperNumber = "somepaper" + cp.Issuer = "someissuer" + + assert.Equal(t, []string{"someissuer", "somepaper"}, cp.GetSplitKey(), "should return issuer and paper number as split key") +} + +func TestSerialize(t *testing.T) { + cp := new(CommercialPaper) + cp.PaperNumber = "somepaper" + cp.Issuer = "someissuer" + cp.IssueDateTime = "sometime" + cp.FaceValue = 1000 + cp.MaturityDateTime = "somelatertime" + cp.Owner = "someowner" + cp.state = TRADING + + bytes, err := cp.Serialize() + assert.Nil(t, err, "should not error on serialize") + assert.Equal(t, `{"paperNumber":"somepaper","issuer":"someissuer","issueDateTime":"sometime","faceValue":1000,"maturityDateTime":"somelatertime","owner":"someowner","currentState":2,"class":"org.papernet.commercialpaper","key":"someissuer:somepaper"}`, string(bytes), "should return JSON formatted value") +} + +func TestDeserialize(t *testing.T) { + var cp *CommercialPaper + var err error + + goodJSON := `{"paperNumber":"somepaper","issuer":"someissuer","issueDateTime":"sometime","faceValue":1000,"maturityDateTime":"somelatertime","owner":"someowner","currentState":2,"class":"org.papernet.commercialpaper","key":"someissuer:somepaper"}` + expectedCp := new(CommercialPaper) + expectedCp.PaperNumber = "somepaper" + expectedCp.Issuer = "someissuer" + expectedCp.IssueDateTime = "sometime" + expectedCp.FaceValue = 1000 + expectedCp.MaturityDateTime = "somelatertime" + expectedCp.Owner = "someowner" + expectedCp.state = TRADING + cp = new(CommercialPaper) + err = Deserialize([]byte(goodJSON), cp) + assert.Nil(t, err, "should not return error for deserialize") + assert.Equal(t, expectedCp, cp, "should create expected commercial paper") + + badJSON := `{"paperNumber":"somepaper","issuer":"someissuer","issueDateTime":"sometime","faceValue":"NaN","maturityDateTime":"somelatertime","owner":"someowner","currentState":2,"class":"org.papernet.commercialpaper","key":"someissuer:somepaper"}` + cp = new(CommercialPaper) + err = Deserialize([]byte(badJSON), cp) + assert.EqualError(t, err, "Error deserializing commercial paper. json: cannot unmarshal string into Go struct field jsonCommercialPaper.faceValue of type int", "should return error for bad data") +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext.go new file mode 100644 index 0000000..c346cf3 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext.go @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// TransactionContextInterface an interface to +// describe the minimum required functions for +// a transaction context in the commercial +// paper +type TransactionContextInterface interface { + contractapi.TransactionContextInterface + GetPaperList() ListInterface +} + +// TransactionContext implementation of +// TransactionContextInterface for use with +// commercial paper contract +type TransactionContext struct { + contractapi.TransactionContext + paperList *list +} + +// GetPaperList return paper list +func (tc *TransactionContext) GetPaperList() ListInterface { + if tc.paperList == nil { + tc.paperList = newList(tc) + } + + return tc.paperList +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext_test.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext_test.go new file mode 100644 index 0000000..7ffc90f --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontext_test.go @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "testing" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/contract-go/ledger-api" + "github.com/stretchr/testify/assert" +) + +func TestGetPaperList(t *testing.T) { + var tc *TransactionContext + var expectedPaperList *list + + tc = new(TransactionContext) + expectedPaperList = newList(tc) + actualList := tc.GetPaperList().(*list) + assert.Equal(t, expectedPaperList.stateList.(*ledgerapi.StateList).Name, actualList.stateList.(*ledgerapi.StateList).Name, "should configure paper list when one not already configured") + + tc = new(TransactionContext) + expectedPaperList = new(list) + expectedStateList := new(ledgerapi.StateList) + expectedStateList.Ctx = tc + expectedStateList.Name = "existing paper list" + expectedPaperList.stateList = expectedStateList + tc.paperList = expectedPaperList + assert.Equal(t, expectedPaperList, tc.GetPaperList(), "should return set paper list when already set") +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract.go new file mode 100644 index 0000000..4e8cee2 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract.go @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// Contract chaincode that defines +// the business logic for managing commercial +// paper +type Contract struct { + contractapi.Contract +} + +// Instantiate does nothing +func (c *Contract) Instantiate() { + fmt.Println("Instantiated") +} + +// Issue creates a new commercial paper and stores it in the world state +func (c *Contract) Issue(ctx TransactionContextInterface, issuer string, paperNumber string, issueDateTime string, maturityDateTime string, faceValue int) (*CommercialPaper, error) { + paper := CommercialPaper{PaperNumber: paperNumber, Issuer: issuer, IssueDateTime: issueDateTime, FaceValue: faceValue, MaturityDateTime: maturityDateTime, Owner: issuer} + paper.SetIssued() + + err := ctx.GetPaperList().AddPaper(&paper) + + if err != nil { + return nil, err + } + + return &paper, nil +} + +// Buy updates a commercial paper to be in trading status and sets the new owner +func (c *Contract) Buy(ctx TransactionContextInterface, issuer string, paperNumber string, currentOwner string, newOwner string, price int, purchaseDateTime string) (*CommercialPaper, error) { + paper, err := ctx.GetPaperList().GetPaper(issuer, paperNumber) + + if err != nil { + return nil, err + } + + if paper.Owner != currentOwner { + return nil, fmt.Errorf("Paper %s:%s is not owned by %s", issuer, paperNumber, currentOwner) + } + + if paper.IsIssued() { + paper.SetTrading() + } + + if !paper.IsTrading() { + return nil, fmt.Errorf("Paper %s:%s is not trading. Current state = %s", issuer, paperNumber, paper.GetState()) + } + + paper.Owner = newOwner + + err = ctx.GetPaperList().UpdatePaper(paper) + + if err != nil { + return nil, err + } + + return paper, nil +} + +// Redeem updates a commercial paper status to be redeemed +func (c *Contract) Redeem(ctx TransactionContextInterface, issuer string, paperNumber string, redeemingOwner string, redeenDateTime string) (*CommercialPaper, error) { + paper, err := ctx.GetPaperList().GetPaper(issuer, paperNumber) + + if err != nil { + return nil, err + } + + if paper.Owner != redeemingOwner { + return nil, fmt.Errorf("Paper %s:%s is not owned by %s", issuer, paperNumber, redeemingOwner) + } + + if paper.IsRedeemed() { + return nil, fmt.Errorf("Paper %s:%s is already redeemed", issuer, paperNumber) + } + + paper.Owner = paper.Issuer + paper.SetRedeemed() + + err = ctx.GetPaperList().UpdatePaper(paper) + + if err != nil { + return nil, err + } + + return paper, nil +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract_test.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract_test.go new file mode 100644 index 0000000..25c429b --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/papercontract_test.go @@ -0,0 +1,185 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "errors" + "testing" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ######### +// HELPERS +// ######### +type MockPaperList struct { + mock.Mock +} + +func (mpl *MockPaperList) AddPaper(paper *CommercialPaper) error { + args := mpl.Called(paper) + + return args.Error(0) +} + +func (mpl *MockPaperList) GetPaper(issuer string, papernumber string) (*CommercialPaper, error) { + args := mpl.Called(issuer, papernumber) + + return args.Get(0).(*CommercialPaper), args.Error(1) +} + +func (mpl *MockPaperList) UpdatePaper(paper *CommercialPaper) error { + args := mpl.Called(paper) + + return args.Error(0) +} + +type MockTransactionContext struct { + contractapi.TransactionContext + paperList *MockPaperList +} + +func (mtc *MockTransactionContext) GetPaperList() ListInterface { + return mtc.paperList +} + +func resetPaper(paper *CommercialPaper) { + paper.Owner = "someowner" + paper.SetTrading() +} + +// ######### +// TESTS +// ######### + +func TestIssue(t *testing.T) { + var paper *CommercialPaper + var err error + + mpl := new(MockPaperList) + ctx := new(MockTransactionContext) + ctx.paperList = mpl + + contract := new(Contract) + + var sentPaper *CommercialPaper + + mpl.On("AddPaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return paper.Issuer == "someissuer" })).Return(nil) + mpl.On("AddPaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return paper.Issuer == "someotherissuer" })).Return(errors.New("AddPaper error")) + + expectedPaper := CommercialPaper{PaperNumber: "somepaper", Issuer: "someissuer", IssueDateTime: "someissuedate", FaceValue: 1000, MaturityDateTime: "somematuritydate", Owner: "someissuer", state: 1} + paper, err = contract.Issue(ctx, "someissuer", "somepaper", "someissuedate", "somematuritydate", 1000) + assert.Nil(t, err, "should not error when add paper does not error") + assert.Equal(t, sentPaper, paper, "should send the same paper as it returns to add paper") + assert.Equal(t, expectedPaper, *paper, "should correctly configure paper") + + paper, err = contract.Issue(ctx, "someotherissuer", "somepaper", "someissuedate", "somematuritydate", 1000) + assert.EqualError(t, err, "AddPaper error", "should return error when add paper fails") + assert.Nil(t, paper, "should not return paper when fails") +} + +func TestBuy(t *testing.T) { + var paper *CommercialPaper + var err error + + mpl := new(MockPaperList) + ctx := new(MockTransactionContext) + ctx.paperList = mpl + + contract := new(Contract) + + wsPaper := new(CommercialPaper) + resetPaper(wsPaper) + + var sentPaper *CommercialPaper + var emptyPaper *CommercialPaper + shouldError := false + + mpl.On("GetPaper", "someissuer", "somepaper").Return(wsPaper, nil) + mpl.On("GetPaper", "someotherissuer", "someotherpaper").Return(emptyPaper, errors.New("GetPaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { return shouldError })).Return(errors.New("UpdatePaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return !shouldError })).Return(nil) + + paper, err = contract.Buy(ctx, "someotherissuer", "someotherpaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "GetPaper error", "should return error when GetPaper errors") + assert.Nil(t, paper, "should return nil for paper when GetPaper errors") + + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someotherowner", "someowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is not owned by someotherowner", "should error when sent owner not correct") + assert.Nil(t, paper, "should not return paper for bad owner error") + + resetPaper(wsPaper) + wsPaper.SetRedeemed() + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is not trading. Current state = REDEEMED") + assert.Nil(t, paper, "should not return paper for bad state error") + + resetPaper(wsPaper) + shouldError = true + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.EqualError(t, err, "UpdatePaper error", "should error when update paper fails") + assert.Nil(t, paper, "should not return paper for bad state error") + shouldError = false + + resetPaper(wsPaper) + wsPaper.SetIssued() + paper, err = contract.Buy(ctx, "someissuer", "somepaper", "someowner", "someotherowner", 100, "2019-12-10:10:00") + assert.Nil(t, err, "should not error when good paper and owner") + assert.Equal(t, "someotherowner", paper.Owner, "should update the owner of the paper") + assert.True(t, paper.IsTrading(), "should mark issued paper as trading") + assert.Equal(t, sentPaper, paper, "should update same paper as it returns in the world state") +} + +func TestRedeem(t *testing.T) { + var paper *CommercialPaper + var err error + + mpl := new(MockPaperList) + ctx := new(MockTransactionContext) + ctx.paperList = mpl + + contract := new(Contract) + + var sentPaper *CommercialPaper + wsPaper := new(CommercialPaper) + resetPaper(wsPaper) + + var emptyPaper *CommercialPaper + shouldError := false + + mpl.On("GetPaper", "someissuer", "somepaper").Return(wsPaper, nil) + mpl.On("GetPaper", "someotherissuer", "someotherpaper").Return(emptyPaper, errors.New("GetPaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { return shouldError })).Return(errors.New("UpdatePaper error")) + mpl.On("UpdatePaper", mock.MatchedBy(func(paper *CommercialPaper) bool { sentPaper = paper; return !shouldError })).Return(nil) + + paper, err = contract.Redeem(ctx, "someotherissuer", "someotherpaper", "someowner", "2021-12-10:10:00") + assert.EqualError(t, err, "GetPaper error", "should error when GetPaper errors") + assert.Nil(t, paper, "should not return paper when GetPaper errors") + + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someotherowner", "2021-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is not owned by someotherowner", "should error when paper owned by someone else") + assert.Nil(t, paper, "should not return paper when errors as owned by someone else") + + resetPaper(wsPaper) + wsPaper.SetRedeemed() + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someowner", "2021-12-10:10:00") + assert.EqualError(t, err, "Paper someissuer:somepaper is already redeemed", "should error when paper already redeemed") + assert.Nil(t, paper, "should not return paper when errors as already redeemed") + + shouldError = true + resetPaper(wsPaper) + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someowner", "2021-12-10:10:00") + assert.EqualError(t, err, "UpdatePaper error", "should error when update paper errors") + assert.Nil(t, paper, "should not return paper when UpdatePaper errors") + shouldError = false + + resetPaper(wsPaper) + paper, err = contract.Redeem(ctx, "someissuer", "somepaper", "someowner", "2021-12-10:10:00") + assert.Nil(t, err, "should not error on good redeem") + assert.True(t, paper.IsRedeemed(), "should return redeemed paper") + assert.Equal(t, sentPaper, paper, "should update same paper as it returns in the world state") +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist.go new file mode 100644 index 0000000..9946d51 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist.go @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/contract-go/ledger-api" + +// ListInterface defines functionality needed +// to interact with the world state on behalf +// of a commercial paper +type ListInterface interface { + AddPaper(*CommercialPaper) error + GetPaper(string, string) (*CommercialPaper, error) + UpdatePaper(*CommercialPaper) error +} + +type list struct { + stateList ledgerapi.StateListInterface +} + +func (cpl *list) AddPaper(paper *CommercialPaper) error { + return cpl.stateList.AddState(paper) +} + +func (cpl *list) GetPaper(issuer string, paperNumber string) (*CommercialPaper, error) { + cp := new(CommercialPaper) + + err := cpl.stateList.GetState(CreateCommercialPaperKey(issuer, paperNumber), cp) + + if err != nil { + return nil, err + } + + return cp, nil +} + +func (cpl *list) UpdatePaper(paper *CommercialPaper) error { + return cpl.stateList.UpdateState(paper) +} + +// NewList create a new list from context +func newList(ctx TransactionContextInterface) *list { + stateList := new(ledgerapi.StateList) + stateList.Ctx = ctx + stateList.Name = "org.papernet.commercialpaperlist" + stateList.Deserialize = func(bytes []byte, state ledgerapi.StateInterface) error { + return Deserialize(bytes, state.(*CommercialPaper)) + } + + list := new(list) + list.stateList = stateList + + return list +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist_test.go b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist_test.go new file mode 100644 index 0000000..c13ff32 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/commercial-paper/paperlist_test.go @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package commercialpaper + +import ( + "errors" + "testing" + + ledgerapi "github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/contract-go/ledger-api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ######### +// HELPERS +// ######### + +type MockStateList struct { + mock.Mock +} + +func (msl *MockStateList) AddState(state ledgerapi.StateInterface) error { + args := msl.Called(state) + + return args.Error(0) +} + +func (msl *MockStateList) GetState(key string, state ledgerapi.StateInterface) error { + args := msl.Called(key, state) + + state.(*CommercialPaper).PaperNumber = "somepaper" + + return args.Error(0) +} + +func (msl *MockStateList) UpdateState(state ledgerapi.StateInterface) error { + args := msl.Called(state) + + return args.Error(0) +} + +// ######### +// TESTS +// ######### + +func TestAddPaper(t *testing.T) { + paper := new(CommercialPaper) + + list := new(list) + msl := new(MockStateList) + msl.On("AddState", paper).Return(errors.New("Called add state correctly")) + list.stateList = msl + + err := list.AddPaper(paper) + assert.EqualError(t, err, "Called add state correctly", "should call state list add state with paper") +} + +func TestGetPaper(t *testing.T) { + var cp *CommercialPaper + var err error + + list := new(list) + msl := new(MockStateList) + msl.On("GetState", CreateCommercialPaperKey("someissuer", "somepaper"), mock.MatchedBy(func(state ledgerapi.StateInterface) bool { _, ok := state.(*CommercialPaper); return ok })).Return(nil) + msl.On("GetState", CreateCommercialPaperKey("someotherissuer", "someotherpaper"), mock.MatchedBy(func(state ledgerapi.StateInterface) bool { _, ok := state.(*CommercialPaper); return ok })).Return(errors.New("GetState error")) + list.stateList = msl + + cp, err = list.GetPaper("someissuer", "somepaper") + assert.Nil(t, err, "should not error when get state on state list does not error") + assert.Equal(t, cp.PaperNumber, "somepaper", "should use state list GetState to fill commercial paper") + + cp, err = list.GetPaper("someotherissuer", "someotherpaper") + assert.EqualError(t, err, "GetState error", "should return error when state list get state errors") + assert.Nil(t, cp, "should not return commercial paper on error") +} + +func TestUpdatePaper(t *testing.T) { + paper := new(CommercialPaper) + + list := new(list) + msl := new(MockStateList) + msl.On("UpdateState", paper).Return(errors.New("Called update state correctly")) + list.stateList = msl + + err := list.UpdatePaper(paper) + assert.EqualError(t, err, "Called update state correctly", "should call state list update state with paper") +} + +func TestNewStateList(t *testing.T) { + ctx := new(TransactionContext) + list := newList(ctx) + stateList, ok := list.stateList.(*ledgerapi.StateList) + + assert.True(t, ok, "should make statelist of type ledgerapi.StateList") + assert.Equal(t, ctx, stateList.Ctx, "should set the context to passed context") + assert.Equal(t, "org.papernet.commercialpaperlist", stateList.Name, "should set the name for the list") + + expectedErr := Deserialize([]byte("bad json"), new(CommercialPaper)) + err := stateList.Deserialize([]byte("bad json"), new(CommercialPaper)) + assert.EqualError(t, err, expectedErr.Error(), "should call Deserialize when stateList.Deserialize called") +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/go.mod b/commercial-paper/organization/magnetocorp/contract-go/go.mod new file mode 100644 index 0000000..97b78f6 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/go.mod @@ -0,0 +1,13 @@ +module github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/contract-go + +go 1.13 + +require ( + github.com/go-openapi/jsonreference v0.19.3 // indirect + github.com/hyperledger/fabric-contract-api-go v1.1.0 + github.com/mailru/easyjson v0.7.0 // indirect + github.com/stretchr/testify v1.5.1 + golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 // indirect + google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 // indirect + google.golang.org/grpc v1.24.0 // indirect +) diff --git a/commercial-paper/organization/magnetocorp/contract-go/go.sum b/commercial-paper/organization/magnetocorp/contract-go/go.sum new file mode 100644 index 0000000..48c8221 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/go.sum @@ -0,0 +1,167 @@ +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/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +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/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +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 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +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/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/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-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-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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 h1:N66aaryRB3Ax92gH0v3hp1QYZ3zWWCCUR/j8Ifh45Ss= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/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-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-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-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-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/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-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 h1:UXl+Zk3jqqcbEVV7ace5lrt4YdA4tXiz3f/KbmD29Vo= +google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +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.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/commercial-paper/organization/magnetocorp/contract-go/ledger-api/state.go b/commercial-paper/organization/magnetocorp/contract-go/ledger-api/state.go new file mode 100644 index 0000000..6d8c3f8 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/ledger-api/state.go @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package ledgerapi + +import ( + "strings" +) + +// SplitKey splits a key on colon +func SplitKey(key string) []string { + return strings.Split(key, ":") +} + +// MakeKey joins key parts using colon +func MakeKey(keyParts ...string) string { + return strings.Join(keyParts, ":") +} + +// StateInterface interface states must implement +// for use in a list +type StateInterface interface { + // GetSplitKey return components that combine to form the key + GetSplitKey() []string + Serialize() ([]byte, error) +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/ledger-api/statelist.go b/commercial-paper/organization/magnetocorp/contract-go/ledger-api/statelist.go new file mode 100644 index 0000000..492efb3 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/ledger-api/statelist.go @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package ledgerapi + +import ( + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// StateListInterface functions that a state list +// should have +type StateListInterface interface { + AddState(StateInterface) error + GetState(string, StateInterface) error + UpdateState(StateInterface) error +} + +// StateList useful for managing putting data in and out +// of the ledger. Implementation of StateListInterface +type StateList struct { + Ctx contractapi.TransactionContextInterface + Name string + Deserialize func([]byte, StateInterface) error +} + +// AddState puts state into world state +func (sl *StateList) AddState(state StateInterface) error { + key, _ := sl.Ctx.GetStub().CreateCompositeKey(sl.Name, state.GetSplitKey()) + data, err := state.Serialize() + + if err != nil { + return err + } + + return sl.Ctx.GetStub().PutState(key, data) +} + +// GetState returns state from world state. Unmarshalls the JSON +// into passed state. Key is the split key value used in Add/Update +// joined using a colon +func (sl *StateList) GetState(key string, state StateInterface) error { + ledgerKey, _ := sl.Ctx.GetStub().CreateCompositeKey(sl.Name, SplitKey(key)) + data, err := sl.Ctx.GetStub().GetState(ledgerKey) + + if err != nil { + return err + } else if data == nil { + return fmt.Errorf("No state found for %s", key) + } + + return sl.Deserialize(data, state) +} + +// UpdateState puts state into world state. Same as AddState but +// separate as semantically different +func (sl *StateList) UpdateState(state StateInterface) error { + return sl.AddState(state) +} diff --git a/commercial-paper/organization/magnetocorp/contract-go/main.go b/commercial-paper/organization/magnetocorp/contract-go/main.go new file mode 100644 index 0000000..ee83834 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-go/main.go @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" + "github.com/hyperledger/fabric-samples/commercial-paper/organization/magnetocorp/contract-go/commercial-paper" +) + +func main() { + + contract := new(commercialpaper.Contract) + contract.TransactionContextHandler = new(commercialpaper.TransactionContext) + contract.Name = "org.papernet.commercialpaper" + contract.Info.Version = "0.0.1" + + chaincode, err := contractapi.NewChaincode(contract) + + if err != nil { + panic(fmt.Sprintf("Error creating chaincode. %s", err.Error())) + } + + chaincode.Info.Title = "CommercialPaperChaincode" + chaincode.Info.Version = "0.0.1" + + err = chaincode.Start() + + if err != nil { + panic(fmt.Sprintf("Error starting chaincode. %s", err.Error())) + } +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/.settings/org.eclipse.buildship.core.prefs b/commercial-paper/organization/magnetocorp/contract-java/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e889521 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/commercial-paper/organization/magnetocorp/contract-java/build.gradle b/commercial-paper/organization/magnetocorp/contract-java/build.gradle new file mode 100644 index 0000000..0a29887 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/build.gradle @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +plugins { + id 'com.github.johnrengelman.shadow' version '5.1.0' + id 'java' +} + + +version '0.0.1' + +sourceCompatibility = 1.8 + +repositories { + mavenLocal() + mavenCentral() + maven { + url 'https://jitpack.io' + } +} + +dependencies { + implementation group: 'org.hyperledger.fabric-chaincode-java', name: 'fabric-chaincode-shim', version: '2.+' + implementation group: 'org.json', name: 'json', version: '20180813' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +shadowJar { + baseName = 'chaincode' + version = null + classifier = null + + manifest { + attributes 'Main-Class': 'org.hyperledger.fabric.contract.ContractRouter' + } +} + + +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-parameters" +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/gradle/wrapper/gradle-wrapper.jar b/commercial-paper/organization/magnetocorp/contract-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/commercial-paper/organization/magnetocorp/contract-java/gradlew.bat b/commercial-paper/organization/magnetocorp/contract-java/gradlew.bat new file mode 100644 index 0000000..24467a1 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/commercial-paper/organization/magnetocorp/contract-java/settings.gradle b/commercial-paper/organization/magnetocorp/contract-java/settings.gradle new file mode 100644 index 0000000..0c5f072 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'papercontract' + diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaper.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaper.java new file mode 100644 index 0000000..13d16b6 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaper.java @@ -0,0 +1,183 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.example; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.example.ledgerapi.State; +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; +import org.json.JSONObject; +import org.json.JSONPropertyIgnore; + +@DataType() +public class CommercialPaper extends State { + + // Enumerate commercial paper state values + public final static String ISSUED = "ISSUED"; + public final static String TRADING = "TRADING"; + public final static String REDEEMED = "REDEEMED"; + + @Property() + private String state=""; + + public String getState() { + return state; + } + + public CommercialPaper setState(String state) { + this.state = state; + return this; + } + + @JSONPropertyIgnore() + public boolean isIssued() { + return this.state.equals(CommercialPaper.ISSUED); + } + + @JSONPropertyIgnore() + public boolean isTrading() { + return this.state.equals(CommercialPaper.TRADING); + } + + @JSONPropertyIgnore() + public boolean isRedeemed() { + return this.state.equals(CommercialPaper.REDEEMED); + } + + public CommercialPaper setIssued() { + this.state = CommercialPaper.ISSUED; + return this; + } + + public CommercialPaper setTrading() { + this.state = CommercialPaper.TRADING; + return this; + } + + public CommercialPaper setRedeemed() { + this.state = CommercialPaper.REDEEMED; + return this; + } + + @Property() + private String paperNumber; + + @Property() + private String issuer; + + @Property() + private String issueDateTime; + + @Property() + private int faceValue; + + @Property() + private String maturityDateTime; + + @Property() + private String owner; + + public String getOwner() { + return owner; + } + + public CommercialPaper setOwner(String owner) { + this.owner = owner; + return this; + } + + public CommercialPaper() { + super(); + } + + public CommercialPaper setKey() { + this.key = State.makeKey(new String[] { this.paperNumber }); + return this; + } + + public String getPaperNumber() { + return paperNumber; + } + + public CommercialPaper setPaperNumber(String paperNumber) { + this.paperNumber = paperNumber; + return this; + } + + public String getIssuer() { + return issuer; + } + + public CommercialPaper setIssuer(String issuer) { + this.issuer = issuer; + return this; + } + + public String getIssueDateTime() { + return issueDateTime; + } + + public CommercialPaper setIssueDateTime(String issueDateTime) { + this.issueDateTime = issueDateTime; + return this; + } + + public int getFaceValue() { + return faceValue; + } + + public CommercialPaper setFaceValue(int faceValue) { + this.faceValue = faceValue; + return this; + } + + public String getMaturityDateTime() { + return maturityDateTime; + } + + public CommercialPaper setMaturityDateTime(String maturityDateTime) { + this.maturityDateTime = maturityDateTime; + return this; + } + + @Override + public String toString() { + return "Paper::" + this.key + " " + this.getPaperNumber() + " " + getIssuer() + " " + getFaceValue(); + } + + /** + * Deserialize a state data to commercial paper + * + * @param {Buffer} data to form back into the object + */ + public static CommercialPaper deserialize(byte[] data) { + JSONObject json = new JSONObject(new String(data, UTF_8)); + + String issuer = json.getString("issuer"); + String paperNumber = json.getString("paperNumber"); + String issueDateTime = json.getString("issueDateTime"); + String maturityDateTime = json.getString("maturityDateTime"); + String owner = json.getString("owner"); + int faceValue = json.getInt("faceValue"); + String state = json.getString("state"); + return createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue,owner,state); + } + + public static byte[] serialize(CommercialPaper paper) { + return State.serialize(paper); + } + + /** + * Factory method to create a commercial paper object + */ + public static CommercialPaper createInstance(String issuer, String paperNumber, String issueDateTime, + String maturityDateTime, int faceValue, String owner, String state) { + return new CommercialPaper().setIssuer(issuer).setPaperNumber(paperNumber).setMaturityDateTime(maturityDateTime) + .setFaceValue(faceValue).setKey().setIssueDateTime(issueDateTime).setOwner(owner).setState(state); + } + + +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContext.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContext.java new file mode 100644 index 0000000..7a946f2 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContext.java @@ -0,0 +1,15 @@ +package org.example; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeStub; + +class CommercialPaperContext extends Context { + + public CommercialPaperContext(ChaincodeStub stub) { + super(stub); + this.paperList = new PaperList(this); + } + + public PaperList paperList; + +} \ No newline at end of file diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContract.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContract.java new file mode 100644 index 0000000..a781c36 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaperContract.java @@ -0,0 +1,170 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ +package org.example; + +import java.util.logging.Logger; + +import org.example.ledgerapi.State; +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contact; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.shim.ChaincodeStub; + +/** + * A custom context provides easy access to list of all commercial papers + */ + +/** + * Define commercial paper smart contract by extending Fabric Contract class + * + */ +@Contract(name = "org.papernet.commercialpaper", info = @Info(title = "MyAsset contract", description = "", version = "0.0.1", license = @License(name = "SPDX-License-Identifier: Apache-2.0", url = ""), contact = @Contact(email = "java-contract@example.com", name = "java-contract", url = "http://java-contract.me"))) +@Default +public class CommercialPaperContract implements ContractInterface { + + // use the classname for the logger, this way you can refactor + private final static Logger LOG = Logger.getLogger(CommercialPaperContract.class.getName()); + + @Override + public Context createContext(ChaincodeStub stub) { + return new CommercialPaperContext(stub); + } + + public CommercialPaperContract() { + + } + + /** + * Define a custom context for commercial paper + */ + + /** + * Instantiate to perform any setup of the ledger that might be required. + * + * @param {Context} ctx the transaction context + */ + @Transaction + public void instantiate(CommercialPaperContext ctx) { + // No implementation required with this example + // It could be where data migration is performed, if necessary + LOG.info("No data migration to perform"); + } + + /** + * Issue commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} issueDateTime paper issue date + * @param {String} maturityDateTime paper maturity date + * @param {Integer} faceValue face value of paper + */ + @Transaction + public CommercialPaper issue(CommercialPaperContext ctx, String issuer, String paperNumber, String issueDateTime, + String maturityDateTime, int faceValue) { + + System.out.println(ctx); + + // create an instance of the paper + CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, + faceValue,issuer,""); + + // Smart contract, rather than paper, moves paper into ISSUED state + paper.setIssued(); + + // Newly issued paper is owned by the issuer + paper.setOwner(issuer); + + System.out.println(paper); + // Add the paper to the list of all similar commercial papers in the ledger + // world state + ctx.paperList.addPaper(paper); + + // Must return a serialized paper to caller of smart contract + return paper; + } + + /** + * Buy commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} currentOwner current owner of paper + * @param {String} newOwner new owner of paper + * @param {Integer} price price paid for this paper + * @param {String} purchaseDateTime time paper was purchased (i.e. traded) + */ + @Transaction + public CommercialPaper buy(CommercialPaperContext ctx, String issuer, String paperNumber, String currentOwner, + String newOwner, int price, String purchaseDateTime) { + + // Retrieve the current paper using key fields provided + String paperKey = State.makeKey(new String[] { paperNumber }); + CommercialPaper paper = ctx.paperList.getPaper(paperKey); + + // Validate current owner + if (!paper.getOwner().equals(currentOwner)) { + throw new RuntimeException("Paper " + issuer + paperNumber + " is not owned by " + currentOwner); + } + + // First buy moves state from ISSUED to TRADING + if (paper.isIssued()) { + paper.setTrading(); + } + + // Check paper is not already REDEEMED + if (paper.isTrading()) { + paper.setOwner(newOwner); + } else { + throw new RuntimeException( + "Paper " + issuer + paperNumber + " is not trading. Current state = " + paper.getState()); + } + + // Update the paper + ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * Redeem commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} redeemingOwner redeeming owner of paper + * @param {String} redeemDateTime time paper was redeemed + */ + @Transaction + public CommercialPaper redeem(CommercialPaperContext ctx, String issuer, String paperNumber, String redeemingOwner, + String redeemDateTime) { + + String paperKey = CommercialPaper.makeKey(new String[] { paperNumber }); + + CommercialPaper paper = ctx.paperList.getPaper(paperKey); + + // Check paper is not REDEEMED + if (paper.isRedeemed()) { + throw new RuntimeException("Paper " + issuer + paperNumber + " already redeemed"); + } + + // Verify that the redeemer owns the commercial paper before redeeming it + if (paper.getOwner().equals(redeemingOwner)) { + paper.setOwner(paper.getIssuer()); + paper.setRedeemed(); + } else { + throw new RuntimeException("Redeeming owner does not own paper" + issuer + paperNumber); + } + + ctx.paperList.updatePaper(paper); + return paper; + } + +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/PaperList.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/PaperList.java new file mode 100644 index 0000000..0ecd24c --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/PaperList.java @@ -0,0 +1,31 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.example; + +import org.example.ledgerapi.StateList; +import org.hyperledger.fabric.contract.Context; + +public class PaperList { + + private StateList stateList; + + public PaperList(Context ctx) { + this.stateList = StateList.getStateList(ctx, PaperList.class.getSimpleName(), CommercialPaper::deserialize); + } + + public PaperList addPaper(CommercialPaper paper) { + stateList.addState(paper); + return this; + } + + public CommercialPaper getPaper(String paperKey) { + return (CommercialPaper) this.stateList.getState(paperKey); + } + + public PaperList updatePaper(CommercialPaper paper) { + this.stateList.updateState(paper); + return this; + } +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/State.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/State.java new file mode 100644 index 0000000..46d35c3 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/State.java @@ -0,0 +1,60 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ +package org.example.ledgerapi; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.json.JSONObject; + +/** + * State class. States have a class, unique key, and a lifecycle current state + * the current state is determined by the specific subclass + */ +public class State { + + protected String key; + + /** + * @param {String|Object} class An identifiable class of the instance + * @param {keyParts[]} elements to pull together to make a key for the objects + */ + public State() { + + } + + String getKey() { + return this.key; + } + + public String[] getSplitKey() { + return State.splitKey(this.key); + } + + /** + * Convert object to buffer containing JSON data serialization Typically used + * before putState()ledger API + * + * @param {Object} JSON object to serialize + * @return {buffer} buffer with the data to store + */ + public static byte[] serialize(Object object) { + String jsonStr = new JSONObject(object).toString(); + return jsonStr.getBytes(UTF_8); + } + + /** + * Join the keyParts to make a unififed string + * + * @param (String[]) keyParts + */ + public static String makeKey(String[] keyParts) { + return String.join(":", keyParts); + } + + public static String[] splitKey(String key) { + System.out.println("splitting key " + key + " " + java.util.Arrays.asList(key.split(":"))); + return key.split(":"); + } + +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java new file mode 100644 index 0000000..891788e --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateDeserializer.java @@ -0,0 +1,6 @@ +package org.example.ledgerapi; + +@FunctionalInterface +public interface StateDeserializer { + State deserialize(byte[] buffer); +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateList.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateList.java new file mode 100644 index 0000000..d672586 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/StateList.java @@ -0,0 +1,48 @@ +package org.example.ledgerapi; + +import org.example.ledgerapi.impl.StateListImpl; +import org.hyperledger.fabric.contract.Context; + +public interface StateList { + + /* + * SPDX-License-Identifier: Apache-2.0 + */ + + /** + * StateList provides a named virtual container for a set of ledger states. Each + * state has a unique key which associates it with the container, rather than + * the container containing a link to the state. This minimizes collisions for + * parallel transactions on different states. + */ + + /** + * Store Fabric context for subsequent API access, and name of list + */ + static StateList getStateList(Context ctx, String listName, StateDeserializer deserializer) { + return new StateListImpl(ctx, listName, deserializer); + } + + /** + * Add a state to the list. Creates a new state in worldstate with appropriate + * composite key. Note that state defines its own key. State object is + * serialized before writing. + */ + public StateList addState(State state); + + /** + * Get a state from the list using supplied keys. Form composite keys to + * retrieve state from world state. State data is deserialized into JSON object + * before being returned. + */ + public State getState(String key); + + /** + * Update a state in the list. Puts the new state in world state with + * appropriate composite key. Note that state defines its own key. A state is + * serialized before writing. Logic is very similar to addState() but kept + * separate becuase it is semantically distinct. + */ + public StateList updateState(State state); + +} diff --git a/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java new file mode 100644 index 0000000..78a4293 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/ledgerapi/impl/StateListImpl.java @@ -0,0 +1,100 @@ +package org.example.ledgerapi.impl; + +import java.util.Arrays; + +import org.example.ledgerapi.State; +import org.example.ledgerapi.StateDeserializer; +import org.example.ledgerapi.StateList; +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.CompositeKey; + +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * StateList provides a named virtual container for a set of ledger states. Each + * state has a unique key which associates it with the container, rather than + * the container containing a link to the state. This minimizes collisions for + * parallel transactions on different states. + */ +public class StateListImpl implements StateList { + + private Context ctx; + private String name; + private Object supportedClasses; + private StateDeserializer deserializer; + + /** + * Store Fabric context for subsequent API access, and name of list + * + * @param deserializer + */ + public StateListImpl(Context ctx, String listName, StateDeserializer deserializer) { + this.ctx = ctx; + this.name = listName; + this.deserializer = deserializer; + + } + + /** + * Add a state to the list. Creates a new state in worldstate with appropriate + * composite key. Note that state defines its own key. State object is + * serialized before writing. + */ + @Override + public StateList addState(State state) { + System.out.println("Adding state " + this.name); + ChaincodeStub stub = this.ctx.getStub(); + System.out.println("Stub=" + stub); + String[] splitKey = state.getSplitKey(); + System.out.println("Split key " + Arrays.asList(splitKey)); + + CompositeKey ledgerKey = stub.createCompositeKey(this.name, splitKey); + System.out.println("ledgerkey is "); + System.out.println(ledgerKey); + + byte[] data = State.serialize(state); + System.out.println("ctx" + this.ctx); + System.out.println("stub" + this.ctx.getStub()); + this.ctx.getStub().putState(ledgerKey.toString(), data); + + return this; + } + + /** + * Get a state from the list using supplied keys. Form composite keys to + * retrieve state from world state. State data is deserialized into JSON object + * before being returned. + */ + @Override + public State getState(String key) { + + CompositeKey ledgerKey = this.ctx.getStub().createCompositeKey(this.name, State.splitKey(key)); + + byte[] data = this.ctx.getStub().getState(ledgerKey.toString()); + if (data != null) { + State state = this.deserializer.deserialize(data); + return state; + } else { + return null; + } + } + + /** + * Update a state in the list. Puts the new state in world state with + * appropriate composite key. Note that state defines its own key. A state is + * serialized before writing. Logic is very similar to addState() but kept + * separate becuase it is semantically distinct. + */ + @Override + public StateList updateState(State state) { + CompositeKey ledgerKey = this.ctx.getStub().createCompositeKey(this.name, state.getSplitKey()); + byte[] data = State.serialize(state); + this.ctx.getStub().putState(ledgerKey.toString(), data); + + return this; + } + +} diff --git a/commercial-paper/organization/magnetocorp/contract/.editorconfig b/commercial-paper/organization/magnetocorp/contract/.editorconfig new file mode 100755 index 0000000..75a13be --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/.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/commercial-paper/organization/magnetocorp/contract/.eslintignore b/commercial-paper/organization/magnetocorp/contract/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/commercial-paper/organization/magnetocorp/contract/.eslintrc.js b/commercial-paper/organization/magnetocorp/contract/.eslintrc.js new file mode 100644 index 0000000..6772c66 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/.eslintrc.js @@ -0,0 +1,37 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +module.exports = { + env: { + node: 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'] + } +}; diff --git a/commercial-paper/organization/magnetocorp/contract/.npmignore b/commercial-paper/organization/magnetocorp/contract/.npmignore new file mode 100644 index 0000000..a00ca94 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/.npmignore @@ -0,0 +1,77 @@ +# +# 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/ + +# 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/commercial-paper/organization/magnetocorp/contract/index.js b/commercial-paper/organization/magnetocorp/contract/index.js new file mode 100644 index 0000000..e0ea2c7 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/index.js @@ -0,0 +1,10 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const cpcontract = require('./lib/papercontract.js'); +module.exports.contracts = [cpcontract]; diff --git a/commercial-paper/organization/magnetocorp/contract/ledger-api/state.js b/commercial-paper/organization/magnetocorp/contract/ledger-api/state.js new file mode 100644 index 0000000..f63e792 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/ledger-api/state.js @@ -0,0 +1,105 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +/** + * State class. States have a class, unique key, and a lifecycle current state + * the current state is determined by the specific subclass + */ +class State { + + /** + * @param {String|Object} class An indentifiable class of the instance + * @param {keyParts[]} elements to pull together to make a key for the objects + */ + constructor(stateClass, keyParts) { + this.class = stateClass; + this.key = State.makeKey(keyParts); + this.currentState = null; + } + + getClass() { + return this.class; + } + + getKey() { + return this.key; + } + + getSplitKey(){ + return State.splitKey(this.key); + } + + getCurrentState(){ + return this.currentState; + } + + // not used +/* serialize() { + + return State.serialize(this); + } */ + + /** + * Convert object to buffer containing JSON data serialization + * Typically used before putState()ledger API + * @param {Object} JSON object to serialize + * @return {buffer} buffer with the data to store + */ + static serialize(object) { + // don't write the key:value passed in - we already have a real composite key, issuer and paper Number. + delete object.key; + return Buffer.from(JSON.stringify(object)); + } + + /** + * Deserialize object into one of a set of supported JSON classes + * i.e. Covert serialized data to JSON object + * Typically used after getState() ledger API + * @param {data} data to deserialize into JSON object + * @param (supportedClasses) the set of classes data can be serialized to + * @return {json} json with the data to store + */ + static deserialize(data, supportedClasses) { + let json = JSON.parse(data.toString()); + let objClass = supportedClasses[json.class]; + if (!objClass) { + throw new Error(`Unknown class of ${json.class}`); + } + let object = new (objClass)(json); + + return object; + } + + /** + * Deserialize object into specific object class + * Typically used after getState() ledger API + * @param {data} data to deserialize into JSON object + * @return {json} json with the data to store + */ + static deserializeClass(data, objClass) { + let json = JSON.parse(data.toString()); + let object = new (objClass)(json); + return object; + } + + /** + * Join the keyParts to make a unififed string + * @param (String[]) keyParts + */ + static makeKey(keyParts) { + // return keyParts.map(part => JSON.stringify(part)).join(':'); + return keyParts.map(part => part).join(':'); + } + + static splitKey(key){ + return key.split(':'); + } + +} + +module.exports = State; diff --git a/commercial-paper/organization/magnetocorp/contract/ledger-api/statelist.js b/commercial-paper/organization/magnetocorp/contract/ledger-api/statelist.js new file mode 100644 index 0000000..15ec837 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/ledger-api/statelist.js @@ -0,0 +1,74 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; +const State = require('./state.js'); + +/** + * StateList provides a named virtual container for a set of ledger states. + * Each state has a unique key which associates it with the container, rather + * than the container containing a link to the state. This minimizes collisions + * for parallel transactions on different states. + */ +class StateList { + + /** + * Store Fabric context for subsequent API access, and name of list + */ + constructor(ctx, listName) { + this.ctx = ctx; + this.name = listName; + this.supportedClasses = {}; + + } + + /** + * Add a state to the list. Creates a new state in worldstate with + * appropriate composite key. Note that state defines its own key. + * State object is serialized before writing. + */ + async addState(state) { + let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey()); + let data = State.serialize(state); + await this.ctx.stub.putState(key, data); + } + + /** + * Get a state from the list using supplied keys. Form composite + * keys to retrieve state from world state. State data is deserialized + * into JSON object before being returned. + */ + async getState(key) { + let ledgerKey = this.ctx.stub.createCompositeKey(this.name, State.splitKey(key)); + let data = await this.ctx.stub.getState(ledgerKey); + if (data && data.toString('utf8')) { + let state = State.deserialize(data, this.supportedClasses); + return state; + } else { + return null; + } + } + + /** + * Update a state in the list. Puts the new state in world state with + * appropriate composite key. Note that state defines its own key. + * A state is serialized before writing. Logic is very similar to + * addState() but kept separate becuase it is semantically distinct. + */ + async updateState(state) { + let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey()); + let data = State.serialize(state); + await this.ctx.stub.putState(key, data); + } + + /** Stores the class for future deserialization */ + use(stateClass) { + this.supportedClasses[stateClass.getClass()] = stateClass; + } + +} + +module.exports = StateList; diff --git a/commercial-paper/organization/magnetocorp/contract/lib/paper.js b/commercial-paper/organization/magnetocorp/contract/lib/paper.js new file mode 100644 index 0000000..0629ba7 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/lib/paper.js @@ -0,0 +1,121 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Utility class for ledger state +const State = require('./../ledger-api/state.js'); + +// Enumerate commercial paper state values +const cpState = { + ISSUED: 1, + PENDING: 2, + TRADING: 3, + REDEEMED: 4 +}; + +/** + * CommercialPaper class extends State class + * Class will be used by application and smart contract to define a paper + */ +class CommercialPaper extends State { + + constructor(obj) { + super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]); + Object.assign(this, obj); + } + + /** + * Basic getters and setters + */ + getIssuer() { + return this.issuer; + } + + setIssuer(newIssuer) { + this.issuer = newIssuer; + } + + getOwner() { + return this.owner; + } + + setOwnerMSP(mspid) { + this.mspid = mspid; + } + + getOwnerMSP() { + return this.mspid; + } + + setOwner(newOwner) { + this.owner = newOwner; + } + + /** + * Useful methods to encapsulate commercial paper states + */ + setIssued() { + this.currentState = cpState.ISSUED; + } + + setTrading() { + this.currentState = cpState.TRADING; + } + + setRedeemed() { + this.currentState = cpState.REDEEMED; + } + + setPending() { + this.currentState = cpState.PENDING; + } + + isIssued() { + return this.currentState === cpState.ISSUED; + } + + isTrading() { + return this.currentState === cpState.TRADING; + } + + isRedeemed() { + return this.currentState === cpState.REDEEMED; + } + + isPending() { + return this.currentState === cpState.PENDING; + } + + static fromBuffer(buffer) { + return CommercialPaper.deserialize(buffer); + } + + toBuffer() { + return Buffer.from(JSON.stringify(this)); + } + + /** + * Deserialize a state data to commercial paper + * @param {Buffer} data to form back into the object + */ + static deserialize(data) { + return State.deserializeClass(data, CommercialPaper); + } + + /** + * Factory method to create a commercial paper object + */ + static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) { + return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue }); + } + + static getClass() { + return 'org.papernet.commercialpaper'; + } +} + +module.exports = CommercialPaper; diff --git a/commercial-paper/organization/magnetocorp/contract/lib/papercontract.js b/commercial-paper/organization/magnetocorp/contract/lib/papercontract.js new file mode 100644 index 0000000..c3d2cdb --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/lib/papercontract.js @@ -0,0 +1,339 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Fabric smart contract classes +const { Contract, Context } = require('fabric-contract-api'); + +// PaperNet specifc classes +const CommercialPaper = require('./paper.js'); +const PaperList = require('./paperlist.js'); +const QueryUtils = require('./queries.js'); + +/** + * A custom context provides easy access to list of all commercial papers + */ +class CommercialPaperContext extends Context { + + constructor() { + super(); + // All papers are held in a list of papers + this.paperList = new PaperList(this); + } + +} + +/** + * Define commercial paper smart contract by extending Fabric Contract class + * + */ +class CommercialPaperContract extends Contract { + + constructor() { + // Unique namespace when multiple contracts per chaincode file + super('org.papernet.commercialpaper'); + } + + /** + * Define a custom context for commercial paper + */ + createContext() { + return new CommercialPaperContext(); + } + + /** + * Instantiate to perform any setup of the ledger that might be required. + * @param {Context} ctx the transaction context + */ + async instantiate(ctx) { + // No implementation required with this example + // It could be where data migration is performed, if necessary + console.log('Instantiate the contract'); + } + + /** + * Issue commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} issueDateTime paper issue date + * @param {String} maturityDateTime paper maturity date + * @param {Integer} faceValue face value of paper + */ + async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) { + + // create an instance of the paper + let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, parseInt(faceValue)); + + // Smart contract, rather than paper, moves paper into ISSUED state + paper.setIssued(); + + // save the owner's MSP + let mspid = ctx.clientIdentity.getMSPID(); + paper.setOwnerMSP(mspid); + + // Newly issued paper is owned by the issuer to begin with (recorded for reporting purposes) + paper.setOwner(issuer); + + // Add the paper to the list of all similar commercial papers in the ledger world state + await ctx.paperList.addPaper(paper); + + // Must return a serialized paper to caller of smart contract + return paper; + } + + /** + * Buy commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} currentOwner current owner of paper + * @param {String} newOwner new owner of paper + * @param {Integer} price price paid for this paper // transaction input - not written to asset + * @param {String} purchaseDateTime time paper was purchased (i.e. traded) // transaction input - not written to asset + */ + async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) { + + // Retrieve the current paper using key fields provided + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + let paper = await ctx.paperList.getPaper(paperKey); + + // Validate current owner + if (paper.getOwner() !== currentOwner) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not owned by ' + currentOwner); + } + + // First buy moves state from ISSUED to TRADING (when running ) + if (paper.isIssued()) { + paper.setTrading(); + } + + // Check paper is not already REDEEMED + if (paper.isTrading()) { + paper.setOwner(newOwner); + // save the owner's MSP + let mspid = ctx.clientIdentity.getMSPID(); + paper.setOwnerMSP(mspid); + } else { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not trading. Current state = ' + paper.getCurrentState()); + } + + // Update the paper + await ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * Buy request: (2-phase confirmation: Commercial paper is 'PENDING' subject to completion of transfer by owning org) + * Alternative to 'buy' transaction + * Note: 'buy_request' puts paper in 'PENDING' state - subject to transfer confirmation [below]. + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} currentOwner current owner of paper + * @param {String} newOwner new owner of paper // transaction input - not written to asset per se - but written to block + * @param {Integer} price price paid for this paper // transaction input - not written to asset per se - but written to block + * @param {String} purchaseDateTime time paper was requested // transaction input - ditto. + */ + async buy_request(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) { + + + // Retrieve the current paper using key fields provided + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + let paper = await ctx.paperList.getPaper(paperKey); + + // Validate current owner - this is really information for the user trying the sample, rather than any 'authorisation' check per se FYI + if (paper.getOwner() !== currentOwner) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not owned by ' + currentOwner + ' provided as a paraneter'); + } + // paper set to 'PENDING' - can only be transferred (confirmed) by identity from owning org (MSP check). + paper.setPending(); + + // Update the paper + await ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * transfer commercial paper: only the owning org has authority to execute. It is the complement to the 'buy_request' transaction. '[]' is optional below. + * eg. issue -> buy_request -> transfer -> [buy ...n | [buy_request...n | transfer ...n] ] -> redeem + * this transaction 'pair' is an alternative to the straight issue -> buy -> [buy....n] -> redeem ...path + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} newOwner new owner of paper + * @param {String} newOwnerMSP MSP id of the transferee + * @param {String} confirmDateTime confirmed transfer date. + */ + async transfer(ctx, issuer, paperNumber, newOwner, newOwnerMSP, confirmDateTime) { + + // Retrieve the current paper using key fields provided + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + let paper = await ctx.paperList.getPaper(paperKey); + + // Validate current owner's MSP in the paper === invoking transferor's MSP id - can only transfer if you are the owning org. + + if (paper.getOwnerMSP() !== ctx.clientIdentity.getMSPID()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not owned by the current invoking Organisation, and not authorised to transfer'); + } + + // Paper needs to be 'pending' - which means you need to have run 'buy_pending' transaction first. + if ( ! paper.isPending()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' is not currently in state: PENDING for transfer to occur: \n must run buy_request transaction first'); + } + // else all good + + paper.setOwner(newOwner); + // set the MSP of the transferee (so that, that org may also pass MSP check, if subsequently transferred/sold on) + paper.setOwnerMSP(newOwnerMSP); + paper.setTrading(); + paper.confirmDateTime = confirmDateTime; + + // Update the paper + await ctx.paperList.updatePaper(paper); + return paper; + } + + /** + * Redeem commercial paper + * + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + * @param {String} redeemingOwner redeeming owner of paper + * @param {String} issuingOwnerMSP the MSP of the org that the paper will be redeemed with. + * @param {String} redeemDateTime time paper was redeemed + */ + async redeem(ctx, issuer, paperNumber, redeemingOwner, issuingOwnerMSP, redeemDateTime) { + + let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); + + let paper = await ctx.paperList.getPaper(paperKey); + + // Check paper is not alread in a state of REDEEMED + if (paper.isRedeemed()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' has already been redeemed'); + } + + // Validate current redeemer's MSP matches the invoking redeemer's MSP id - can only redeem if you are the owning org. + + if (paper.getOwnerMSP() !== ctx.clientIdentity.getMSPID()) { + throw new Error('\nPaper ' + issuer + paperNumber + ' cannot be redeemed by ' + ctx.clientIdentity.getMSPID() + ', as it is not the authorised owning Organisation'); + } + + // As this is just a sample, can show additional verification check: that the redeemer provided matches that on record, before redeeming it + if (paper.getOwner() === redeemingOwner) { + paper.setOwner(paper.getIssuer()); + paper.setOwnerMSP(issuingOwnerMSP); + paper.setRedeemed(); + paper.redeemDateTime = redeemDateTime; // record redemption date against the asset (the complement to 'issue date') + } else { + throw new Error('\nRedeeming owner: ' + redeemingOwner + ' organisation does not currently own paper: ' + issuer + paperNumber); + } + + await ctx.paperList.updatePaper(paper); + return paper; + } + + // Query transactions + + /** + * Query history of a commercial paper + * @param {Context} ctx the transaction context + * @param {String} issuer commercial paper issuer + * @param {Integer} paperNumber paper number for this issuer + */ + async queryHistory(ctx, issuer, paperNumber) { + + // Get a key to be used for History query + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let results = await query.getAssetHistory(issuer, paperNumber); // (cpKey); + return results; + + } + + /** + * queryOwner commercial paper: supply name of owning org, to find list of papers based on owner field + * @param {Context} ctx the transaction context + * @param {String} owner commercial paper owner + */ + async queryOwner(ctx, owner) { + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let owner_results = await query.queryKeyByOwner(owner); + + return owner_results; + } + + /** + * queryPartial commercial paper - provide a prefix eg. "DigiBank" will list all papers _issued_ by DigiBank etc etc + * @param {Context} ctx the transaction context + * @param {String} prefix asset class prefix (added to paperlist namespace) eg. org.papernet.paperMagnetoCorp asset listing: papers issued by MagnetoCorp. + */ + async queryPartial(ctx, prefix) { + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let partial_results = await query.queryKeyByPartial(prefix); + + return partial_results; + } + + /** + * queryAdHoc commercial paper - supply a custom mango query + * eg - as supplied as a param: + * ex1: ["{\"selector\":{\"faceValue\":{\"$lt\":8000000}}}"] + * ex2: ["{\"selector\":{\"faceValue\":{\"$gt\":4999999}}}"] + * + * @param {Context} ctx the transaction context + * @param {String} queryString querystring + */ + async queryAdhoc(ctx, queryString) { + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let querySelector = JSON.parse(queryString); + let adhoc_results = await query.queryByAdhoc(querySelector); + + return adhoc_results; + } + + + /** + * queryNamed - supply named query - 'case' statement chooses selector to build (pre-canned for demo purposes) + * @param {Context} ctx the transaction context + * @param {String} queryname the 'named' query (built here) - or - the adHoc query string, provided as a parameter + */ + async queryNamed(ctx, queryname) { + let querySelector = {}; + switch (queryname) { + case "redeemed": + querySelector = { "selector": { "currentState": 4 } }; // 4 = redeemd state + break; + case "trading": + querySelector = { "selector": { "currentState": 3 } }; // 3 = trading state + break; + case "value": + // may change to provide as a param - fixed value for now in this sample + querySelector = { "selector": { "faceValue": { "$gt": 4000000 } } }; // to test, issue CommPapers with faceValue <= or => this figure. + break; + default: // else, unknown named query + throw new Error('invalid named query supplied: ' + queryname + '- please try again '); + } + + let query = new QueryUtils(ctx, 'org.papernet.paper'); + let adhoc_results = await query.queryByAdhoc(querySelector); + + return adhoc_results; + } + +} + +module.exports = CommercialPaperContract; diff --git a/commercial-paper/organization/magnetocorp/contract/lib/paperlist.js b/commercial-paper/organization/magnetocorp/contract/lib/paperlist.js new file mode 100644 index 0000000..73528b6 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/lib/paperlist.js @@ -0,0 +1,35 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +// Utility class for collections of ledger states -- a state list +const StateList = require('./../ledger-api/statelist.js'); + +const CommercialPaper = require('./paper.js'); + +class PaperList extends StateList { + + constructor(ctx) { + super(ctx, 'org.papernet.paper'); + this.use(CommercialPaper); + } + + async addPaper(paper) { + return this.addState(paper); + } + + async getPaper(paperKey) { + return this.getState(paperKey); + } + + async updatePaper(paper) { + return this.updateState(paper); + } +} + + +module.exports = PaperList; diff --git a/commercial-paper/organization/magnetocorp/contract/lib/queries.js b/commercial-paper/organization/magnetocorp/contract/lib/queries.js new file mode 100644 index 0000000..a7cd40a --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/lib/queries.js @@ -0,0 +1,215 @@ + +/* +SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const State = require('../ledger-api/state.js'); +//const CommercialPaper = require('./paper.js'); +/** + * Query Class for query functions such as history etc + * + */ +class QueryUtils { + + constructor(ctx, listName) { + this.ctx = ctx; + this.name = listName; + //this.supportedTypes = {}; + } + + // ========================================================================================= + // getAssetHistory takes the composite key as arg, gets returns results as JSON to 'main contract' + // ========================================================================================= + /** + * Get Asset History for a commercial paper + * @param {String} issuer the CP issuer + * @param {String} paperNumber commercial paper number + */ + async getAssetHistory(issuer, paperNumber) { + + let ledgerKey = await this.ctx.stub.createCompositeKey(this.name, [issuer, paperNumber]); + const resultsIterator = await this.ctx.stub.getHistoryForKey(ledgerKey); + let results = await this.getAllResults(resultsIterator, true); + + return results; + } + + // =========================================================================================== + // queryKeyByPartial performs a partial query based on the namespace and asset key prefix provided + + // Read-only function results are not typically submitted to ordering. If the read-only + // results are submitted to ordering, or if the query is used in an update transaction + // and submitted to ordering, then the committing peers will re-execute to guarantee that + // result sets are stable between endorsement time and commit time. The transaction is + // invalidated by the committing peers if the result set has changed between endorsement + // time and commit time. + // + // =========================================================================================== + /** + * queryOwner commercial paper + * @param {String} assetspace the asset space (eg MagnetoCorp's assets) + */ + async queryKeyByPartial(assetspace) { + + if (arguments.length < 1) { + throw new Error('Incorrect number of arguments. Expecting 1'); + } + // ie namespace + prefix to assets etc eg + // "Key":"org.papernet.paperMagnetoCorp0001" (0002, etc) + // "Partial":'org.papernet.paperlistMagnetoCorp"' (using partial key, find keys "0001", "0002" etc) + const resultsIterator = await this.ctx.stub.getStateByPartialCompositeKey(this.name, [assetspace]); + let method = this.getAllResults; + let results = await method(resultsIterator, false); + + return results; + } + + + // ===== Example: Parameterized rich query ================================================= + // queryKeyByOwner queries for assets based on a passed in owner. + // This is an example of a parameterized query accepting a single query parameter (owner). + // Only available on state databases that support rich query (e.g. CouchDB) + // ========================================================================================= + /** + * queryKeyByOwner commercial paper + * @param {String} owner commercial paper owner + */ + async queryKeyByOwner(owner) { + // + let self = this; + if (arguments.length < 1) { + throw new Error('Incorrect number of arguments. Expecting owner name.'); + } + let queryString = {}; + queryString.selector = {}; + // queryString.selector.docType = 'indexOwnerDoc'; + queryString.selector.owner = owner; + // set to (eg) '{selector:{owner:MagnetoCorp}}' + let method = self.getQueryResultForQueryString; + let queryResults = await method(this.ctx, self, JSON.stringify(queryString)); + return queryResults; + } + + // ===== Example: Ad hoc rich query ======================================================== + // queryAdhoc uses a query string to perform a query for marbles.. + // Query string matching state database syntax is passed in and executed as is. + // Supports ad hoc queries that can be defined at runtime by the client. + // If this is not desired, follow the queryKeyByOwner example for parameterized queries. + // Only available on state databases that support rich query (e.g. CouchDB) + // example passed using VS Code ext: ["{\"selector\": {\"owner\": \"MagnetoCorp\"}}"] + // ========================================================================================= + /** + * query By AdHoc string (commercial paper) + * @param {String} queryString actual MangoDB query string (escaped) + */ + async queryByAdhoc(queryString) { + + if (arguments.length < 1) { + throw new Error('Incorrect number of arguments. Expecting ad-hoc string, which gets stringified for mango query'); + } + let self = this; + + if (!queryString) { + throw new Error('queryString must not be empty'); + } + let method = self.getQueryResultForQueryString; + let queryResults = await method(this.ctx, self, JSON.stringify(queryString)); + return queryResults; + } + + // WORKER functions are below this line: these are called by the above functions, where iterator is passed in + + // ========================================================================================= + // getQueryResultForQueryString woerk function executes the passed-in query string. + // Result set is built and returned as a byte array containing the JSON results. + // ========================================================================================= + /** + * Function getQueryResultForQueryString + * @param {Context} ctx the transaction context + * @param {any} self within scope passed in + * @param {String} the query string created prior to calling this fn + */ + async getQueryResultForQueryString(ctx, self, queryString) { + + // console.log('- getQueryResultForQueryString queryString:\n' + queryString); + + const resultsIterator = await ctx.stub.getQueryResult(queryString); + let results = await self.getAllResults(resultsIterator, false); + + return results; + + } + + /** + * Function getAllResults + * @param {resultsIterator} iterator within scope passed in + * @param {Boolean} isHistory query string created prior to calling this fn + */ + async getAllResults(iterator, isHistory) { + let allResults = []; + let res = { done: false, value: null }; + + while (true) { + res = await iterator.next(); + let jsonRes = {}; + if (res.value && res.value.value.toString()) { + if (isHistory && isHistory === true) { + //jsonRes.TxId = res.value.tx_id; + jsonRes.TxId = res.value.txId; + jsonRes.Timestamp = res.value.timestamp; + jsonRes.Timestamp = new Date((res.value.timestamp.seconds.low * 1000)); + let ms = res.value.timestamp.nanos / 1000000; + jsonRes.Timestamp.setMilliseconds(ms); + if (res.value.is_delete) { + jsonRes.IsDelete = res.value.is_delete.toString(); + } else { + try { + jsonRes.Value = JSON.parse(res.value.value.toString('utf8')); + // report the commercial paper states during the asset lifecycle, just for asset history reporting + switch (jsonRes.Value.currentState) { + case 1: + jsonRes.Value.currentState = 'ISSUED'; + break; + case 2: + jsonRes.Value.currentState = 'PENDING'; + break; + case 3: + jsonRes.Value.currentState = 'TRADING'; + break; + case 4: + jsonRes.Value.currentState = 'REDEEMED'; + break; + default: // else, unknown named query + jsonRes.Value.currentState = 'UNKNOWN'; + } + + } catch (err) { + console.log(err); + jsonRes.Value = res.value.value.toString('utf8'); + } + } + } else { // non history query .. + jsonRes.Key = res.value.key; + try { + jsonRes.Record = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Record = res.value.value.toString('utf8'); + } + } + allResults.push(jsonRes); + } + // check to see if we have reached the end + if (res.done) { + // explicitly close the iterator + console.log('iterator is done'); + await iterator.close(); + return allResults; + } + + } // while true + } + +} +module.exports = QueryUtils; \ No newline at end of file diff --git a/commercial-paper/organization/magnetocorp/contract/package.json b/commercial-paper/organization/magnetocorp/contract/package.json new file mode 100644 index 0000000..2c4a644 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/package.json @@ -0,0 +1,49 @@ +{ + "name": "papernet-js", + "version": "0.0.1", + "description": "Papernet Contract", + "main": "index.js", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha test --recursive", + "start": "fabric-chaincode-node start", + "mocha": "mocha test --recursive" + }, + "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": "^5.2.0", + "nyc": "^12.0.2", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/commercial-paper/organization/magnetocorp/contract/test/contract.js b/commercial-paper/organization/magnetocorp/contract/test/contract.js new file mode 100644 index 0000000..63576ee --- /dev/null +++ b/commercial-paper/organization/magnetocorp/contract/test/contract.js @@ -0,0 +1,43 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const Chaincode = require('../lib/chaincode'); +const { Stub } = require('fabric-shim'); + +require('chai').should(); +const sinon = require('sinon'); + +describe('Chaincode', () => { + + describe('#Init', () => { + + it('should work', async () => { + const cc = new Chaincode(); + const stub = sinon.createStubInstance(Stub); + stub.getFunctionAndParameters.returns({ fcn: 'initFunc', params: [] }); + const res = await cc.Init(stub); + res.status.should.equal(Stub.RESPONSE_CODE.OK); + }); + + }); + + describe('#Invoke', async () => { + + it('should work', async () => { + const cc = new Chaincode(); + const stub = sinon.createStubInstance(Stub); + stub.getFunctionAndParameters.returns({ fcn: 'initFunc', params: [] }); + let res = await cc.Init(stub); + res.status.should.equal(Stub.RESPONSE_CODE.OK); + stub.getFunctionAndParameters.returns({ fcn: 'invokeFunc', params: [] }); + res = await cc.Invoke(stub); + res.status.should.equal(Stub.RESPONSE_CODE.OK); + }); + + }); + +}); diff --git a/commercial-paper/organization/magnetocorp/gateway/.gitkeep b/commercial-paper/organization/magnetocorp/gateway/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/commercial-paper/organization/magnetocorp/magnetocorp.sh b/commercial-paper/organization/magnetocorp/magnetocorp.sh new file mode 100755 index 0000000..784602c --- /dev/null +++ b/commercial-paper/organization/magnetocorp/magnetocorp.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# +# SPDX-License-Identifier: Apache-2.0 +shopt -s extglob + +function _exit(){ + printf "Exiting:%s\n" "$1" + exit -1 +} + + +: ${CHANNEL_NAME:="mychannel"} +: ${DELAY:="3"} +: ${MAX_RETRY:="5"} +: ${VERBOSE:="false"} + +# Where am I? +DIR=${PWD} + +# Locate the test-network +cd "${DIR}/../../../test-network" +env | sort > /tmp/env.orig + +OVERRIDE_ORG="2" +. ./scripts/envVar.sh + + +parsePeerConnectionParameters 1 2 +export PEER_PARMS="${PEER_CONN_PARMS##*( )}" + +# set the fabric config path +export FABRIC_CFG_PATH="${DIR}/../../../config" +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} diff --git a/commercial-paper/organization/magnetocorp/t.js b/commercial-paper/organization/magnetocorp/t.js new file mode 100644 index 0000000..031abd3 --- /dev/null +++ b/commercial-paper/organization/magnetocorp/t.js @@ -0,0 +1 @@ +console.log(process.argv); \ No newline at end of file diff --git a/fabcar/.gitignore b/fabcar/.gitignore new file mode 100644 index 0000000..1e3d188 --- /dev/null +++ b/fabcar/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/package-lock.json +/hfc-key-store/ diff --git a/fabcar/go/.gitignore b/fabcar/go/.gitignore new file mode 100755 index 0000000..076e1be --- /dev/null +++ b/fabcar/go/.gitignore @@ -0,0 +1,2 @@ +wallet +!wallet/.gitkeep \ No newline at end of file diff --git a/fabcar/go/fabcar.go b/fabcar/go/fabcar.go new file mode 100644 index 0000000..0b1ffb9 --- /dev/null +++ b/fabcar/go/fabcar.go @@ -0,0 +1,141 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" +) + +func main() { + os.Setenv("DISCOVERY_AS_LOCALHOST", "true") + wallet, err := gateway.NewFileSystemWallet("wallet") + if err != nil { + fmt.Printf("Failed to create wallet: %s\n", err) + os.Exit(1) + } + + if !wallet.Exists("appUser") { + err = populateWallet(wallet) + if err != nil { + fmt.Printf("Failed to populate wallet contents: %s\n", err) + os.Exit(1) + } + } + + 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 { + fmt.Printf("Failed to connect to gateway: %s\n", err) + os.Exit(1) + } + defer gw.Close() + + network, err := gw.GetNetwork("mychannel") + if err != nil { + fmt.Printf("Failed to get network: %s\n", err) + os.Exit(1) + } + + contract := network.GetContract("fabcar") + + result, err := contract.EvaluateTransaction("queryAllCars") + if err != nil { + fmt.Printf("Failed to evaluate transaction: %s\n", err) + os.Exit(1) + } + fmt.Println(string(result)) + + result, err = contract.SubmitTransaction("createCar", "CAR10", "VW", "Polo", "Grey", "Mary") + if err != nil { + fmt.Printf("Failed to submit transaction: %s\n", err) + os.Exit(1) + } + fmt.Println(string(result)) + + result, err = contract.EvaluateTransaction("queryCar", "CAR10") + if err != nil { + fmt.Printf("Failed to evaluate transaction: %s\n", err) + os.Exit(1) + } + fmt.Println(string(result)) + + _, err = contract.SubmitTransaction("changeCarOwner", "CAR10", "Archie") + if err != nil { + fmt.Printf("Failed to submit transaction: %s\n", err) + os.Exit(1) + } + + result, err = contract.EvaluateTransaction("queryCar", "CAR10") + if err != nil { + fmt.Printf("Failed to evaluate transaction: %s\n", err) + os.Exit(1) + } + fmt.Println(string(result)) +} + +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 errors.New("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)) + + err = wallet.Put("appUser", identity) + if err != nil { + return err + } + return nil +} diff --git a/fabcar/go/go.mod b/fabcar/go/go.mod new file mode 100644 index 0000000..d23a5bc --- /dev/null +++ b/fabcar/go/go.mod @@ -0,0 +1,5 @@ +module fabcar + +go 1.14 + +require github.com/hyperledger/fabric-sdk-go v1.0.0-rc1 diff --git a/fabcar/go/go.sum b/fabcar/go/go.sum new file mode 100644 index 0000000..4d2133c --- /dev/null +++ b/fabcar/go/go.sum @@ -0,0 +1,126 @@ +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/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/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +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.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +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= +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.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= +github.com/golang/mock v1.2.0/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/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/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +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-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/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/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= +github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +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/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= +github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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/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_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/common v0.0.0-20180518154759-7600349dcfe1 h1:osmNoEW2SCW3L7EX0km2LYM8HKpNWRiouxjE3XHkyGc= +github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +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/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= +github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +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/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= +github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/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/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-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/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-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-20180909124046-d0be0721c37e/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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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/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= +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= +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= diff --git a/fabcar/go/runfabcar.sh b/fabcar/go/runfabcar.sh new file mode 100755 index 0000000..1379252 --- /dev/null +++ b/fabcar/go/runfabcar.sh @@ -0,0 +1,15 @@ + +ENV_DAL=`echo $DISCOVERY_AS_LOCALHOST` + +echo "ENV_DAL:"$DISCOVERY_AS_LOCALHOST + +if [ "$ENV_DAL" != "true" ] +then + export DISCOVERY_AS_LOCALHOST=true +fi + +echo "DISCOVERY_AS_LOCALHOST="$DISCOVERY_AS_LOCALHOST + +echo "run fabcar..." + +go run fabcar.go diff --git a/fabcar/java/.gitignore b/fabcar/java/.gitignore new file mode 100755 index 0000000..3aa04dc --- /dev/null +++ b/fabcar/java/.gitignore @@ -0,0 +1,7 @@ +/bin/ +/target/ +.settings/ +.classpath +.project +wallet +!wallet/.gitkeep \ No newline at end of file diff --git a/fabcar/java/pom.xml b/fabcar/java/pom.xml new file mode 100644 index 0000000..9bdcb65 --- /dev/null +++ b/fabcar/java/pom.xml @@ -0,0 +1,85 @@ + + 4.0.0 + fabcar-java + fabcar-java + 2.1.1 + + + + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.0 + + + + package + + shade + + + + + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + oss-sonatype + OSS Sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + + + org.hyperledger.fabric + fabric-gateway-java + 2.1.1 + + + org.junit.platform + junit-platform-launcher + 1.4.2 + + + org.junit.jupiter + junit-jupiter-engine + 5.4.1 + test + + + org.junit.vintage + junit-vintage-engine + 5.4.2 + + + org.assertj + assertj-core + 3.12.2 + test + + + diff --git a/fabcar/java/src/main/java/org/example/ClientApp.java b/fabcar/java/src/main/java/org/example/ClientApp.java new file mode 100755 index 0000000..d4c6fee --- /dev/null +++ b/fabcar/java/src/main/java/org/example/ClientApp.java @@ -0,0 +1,56 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.example; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.hyperledger.fabric.gateway.Contract; +import org.hyperledger.fabric.gateway.Gateway; +import org.hyperledger.fabric.gateway.Network; +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; + +public class ClientApp { + + static { + System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true"); + } + + public static void main(String[] args) throws Exception { + // Load a file system based wallet for managing identities. + Path walletPath = Paths.get("wallet"); + Wallet wallet = Wallets.newFileSystemWallet(walletPath); + // load a CCP + Path networkConfigPath = Paths.get("..", "..", "test-network", "organizations", "peerOrganizations", "org1.example.com", "connection-org1.yaml"); + + Gateway.Builder builder = Gateway.createBuilder(); + builder.identity(wallet, "appUser").networkConfig(networkConfigPath).discovery(true); + + // create a gateway connection + try (Gateway gateway = builder.connect()) { + + // get the network and contract + Network network = gateway.getNetwork("mychannel"); + Contract contract = network.getContract("fabcar"); + + byte[] result; + + result = contract.evaluateTransaction("queryAllCars"); + System.out.println(new String(result)); + + contract.submitTransaction("createCar", "CAR10", "VW", "Polo", "Grey", "Mary"); + + result = contract.evaluateTransaction("queryCar", "CAR10"); + System.out.println(new String(result)); + + contract.submitTransaction("changeCarOwner", "CAR10", "Archie"); + + result = contract.evaluateTransaction("queryCar", "CAR10"); + System.out.println(new String(result)); + } + } + +} diff --git a/fabcar/java/src/main/java/org/example/EnrollAdmin.java b/fabcar/java/src/main/java/org/example/EnrollAdmin.java new file mode 100644 index 0000000..8b3e2f5 --- /dev/null +++ b/fabcar/java/src/main/java/org/example/EnrollAdmin.java @@ -0,0 +1,55 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.example; + +import java.nio.file.Paths; +import java.util.Properties; + +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Identity; +import org.hyperledger.fabric.sdk.Enrollment; +import org.hyperledger.fabric.sdk.security.CryptoSuite; +import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory; +import org.hyperledger.fabric_ca.sdk.EnrollmentRequest; +import org.hyperledger.fabric_ca.sdk.HFCAClient; + +public class EnrollAdmin { + + static { + System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true"); + } + + public static void main(String[] args) throws Exception { + + // Create a CA client for interacting with the CA. + Properties props = new Properties(); + props.put("pemFile", + "../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem"); + props.put("allowAllHostNames", "true"); + HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props); + CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite(); + caClient.setCryptoSuite(cryptoSuite); + + // Create a wallet for managing identities + Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet")); + + // Check to see if we've already enrolled the admin user. + if (wallet.get("admin") != null) { + System.out.println("An identity for the admin user \"admin\" already exists in the wallet"); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + final EnrollmentRequest enrollmentRequestTLS = new EnrollmentRequest(); + enrollmentRequestTLS.addHost("localhost"); + enrollmentRequestTLS.setProfile("tls"); + Enrollment enrollment = caClient.enroll("admin", "adminpw", enrollmentRequestTLS); + Identity user = Identities.newX509Identity("Org1MSP", enrollment); + wallet.put("admin", user); + System.out.println("Successfully enrolled user \"admin\" and imported it into the wallet"); + } +} diff --git a/fabcar/java/src/main/java/org/example/RegisterUser.java b/fabcar/java/src/main/java/org/example/RegisterUser.java new file mode 100644 index 0000000..c116df4 --- /dev/null +++ b/fabcar/java/src/main/java/org/example/RegisterUser.java @@ -0,0 +1,111 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.example; + +import java.nio.file.Paths; +import java.security.PrivateKey; +import java.util.Properties; +import java.util.Set; + +import org.hyperledger.fabric.gateway.Wallet; +import org.hyperledger.fabric.gateway.Wallets; +import org.hyperledger.fabric.gateway.Identities; +import org.hyperledger.fabric.gateway.Identity; +import org.hyperledger.fabric.gateway.X509Identity; +import org.hyperledger.fabric.sdk.Enrollment; +import org.hyperledger.fabric.sdk.User; +import org.hyperledger.fabric.sdk.security.CryptoSuite; +import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory; +import org.hyperledger.fabric_ca.sdk.HFCAClient; +import org.hyperledger.fabric_ca.sdk.RegistrationRequest; + +public class RegisterUser { + + static { + System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true"); + } + + public static void main(String[] args) throws Exception { + + // Create a CA client for interacting with the CA. + Properties props = new Properties(); + props.put("pemFile", + "../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem"); + props.put("allowAllHostNames", "true"); + HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props); + CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite(); + caClient.setCryptoSuite(cryptoSuite); + + // Create a wallet for managing identities + Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet")); + + // Check to see if we've already enrolled the user. + if (wallet.get("appUser") != null) { + System.out.println("An identity for the user \"appUser\" already exists in the wallet"); + return; + } + + X509Identity adminIdentity = (X509Identity)wallet.get("admin"); + if (adminIdentity == null) { + System.out.println("\"admin\" needs to be enrolled and added to the wallet first"); + return; + } + User admin = new User() { + + @Override + public String getName() { + return "admin"; + } + + @Override + public Set getRoles() { + return null; + } + + @Override + public String getAccount() { + return null; + } + + @Override + public String getAffiliation() { + return "org1.department1"; + } + + @Override + public Enrollment getEnrollment() { + return new Enrollment() { + + @Override + public PrivateKey getKey() { + return adminIdentity.getPrivateKey(); + } + + @Override + public String getCert() { + return Identities.toPemString(adminIdentity.getCertificate()); + } + }; + } + + @Override + public String getMspId() { + return "Org1MSP"; + } + + }; + + // Register the user, enroll the user, and import the new identity into the wallet. + RegistrationRequest registrationRequest = new RegistrationRequest("appUser"); + registrationRequest.setAffiliation("org1.department1"); + registrationRequest.setEnrollmentID("appUser"); + String enrollmentSecret = caClient.register(registrationRequest, admin); + Enrollment enrollment = caClient.enroll("appUser", enrollmentSecret); + Identity user = Identities.newX509Identity("Org1MSP", adminIdentity.getCertificate(), adminIdentity.getPrivateKey()); + wallet.put("appUser", user); + System.out.println("Successfully enrolled user \"appUser\" and imported it into the wallet"); + } + +} diff --git a/fabcar/java/src/test/java/org/example/ClientTest.java b/fabcar/java/src/test/java/org/example/ClientTest.java new file mode 100644 index 0000000..4bf2d83 --- /dev/null +++ b/fabcar/java/src/test/java/org/example/ClientTest.java @@ -0,0 +1,17 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +package org.example; + +import org.junit.Test; + +public class ClientTest { + + @Test + public void testFabCar() throws Exception { + EnrollAdmin.main(null); + RegisterUser.main(null); + ClientApp.main(null); + } +} diff --git a/fabcar/javascript/.editorconfig b/fabcar/javascript/.editorconfig new file mode 100755 index 0000000..75a13be --- /dev/null +++ b/fabcar/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/fabcar/javascript/.eslintignore b/fabcar/javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/fabcar/javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/fabcar/javascript/.eslintrc.js b/fabcar/javascript/.eslintrc.js new file mode 100644 index 0000000..8b83df7 --- /dev/null +++ b/fabcar/javascript/.eslintrc.js @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + env: { + node: 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'] + } +}; diff --git a/fabcar/javascript/.gitignore b/fabcar/javascript/.gitignore new file mode 100644 index 0000000..1dcaf1f --- /dev/null +++ b/fabcar/javascript/.gitignore @@ -0,0 +1,80 @@ +# +# 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/ + +# 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 + +wallet +!wallet/.gitkeep diff --git a/fabcar/javascript/EncryptDecrypt-v2.js b/fabcar/javascript/EncryptDecrypt-v2.js new file mode 100644 index 0000000..df125dc --- /dev/null +++ b/fabcar/javascript/EncryptDecrypt-v2.js @@ -0,0 +1,274 @@ +const crypto = require("crypto") +const fs = require('fs'); + +// Including generateKeyPair from crypto module +const { generateKeyPair } = require('crypto'); + +// first we need two keys here - index key and private key. Private key will +// be generated only once and then somehow store it + +let indexKey = "9uZlGXa0o64kdbQ6Gb96qw==" + +let publicKeyPath = './keys/publicKey.key' +let privateKeyPath = './keys/privateKey.key' + + +// Calling generateKeyPair() method +// with its parameters +exports.generateKeys = () => { + + return new Promise( (resolve, reject) => { + + generateKeyPair('rsa', { + modulusLength: 2048, // options + publicExponent: 0x10101, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-192-cbc', + passphrase: 'sse' + } + }, (err, PublicKey, PrivateKey) => { // Callback function + if(!err) + { + fs.writeFile(publicKeyPath, PublicKey.toString('hex'), (er) => { + if (er) console.log(er); + console.log("Key Successfully Saved."); + }); + + fs.writeFile(privateKeyPath, PrivateKey.toString('hex'), (er) => { + if (er) console.log(er); + console.log("Key Successfully Saved."); + }); + resolve(); + + } + else + { + // Prints error if any + console.log("Errr is: ", err); + reject(); + } + }); + + }) + +}; + + +exports.getKeywordHash = (keyword) => { + let keyword_hash = crypto.createHmac('sha256', indexKey) + .update(keyword) + .digest('hex') + return keyword_hash + }; + +exports.getEncryptionKeyword = (pbKey, keyword) => { + //Also pass publicKey.key the file here as a parameter + //let pbKey = fs.readFileSync('./keys/publicKey.key'); + + const encryptedKeyword = crypto.publicEncrypt( + { + key: pbKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: "sha256", + }, + + Buffer.from(keyword) + ) + + //console.log("encypted data: ", encryptedKeyword.toString('hex')); + return encryptedKeyword.toString('hex'); +} + + +exports.getDecryptionKeyword = (encryptedKeyword) => { + let prKey = fs.readFileSync('./keys/privateKey.key'); + + const keyword = crypto.privateDecrypt( + { + key: prKey, + passphrase: 'sse', + padding: crypto.constants.RSA_PKCS8_OAEP_PADDING, + oaepHash: "sha256", + }, + Buffer.from(encryptedKeyword, 'hex') + ) + + //console.log("decrypted data: ", keyword.toString()); + return keyword.toString(); +}; + +exports.getEncryptFile = (pbKey, filePath) => { + //Store publicKey.key as key file + //Also pass publicKey.key the file here as a parameter + //let pbKey = fs.readFileSync('./keys/publicKey.key'); + + fs.readFile(filePath, "utf-8", (err, data) => { + + const encryptedData = crypto.publicEncrypt( + { + key: pbKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: "sha256", + }, + + Buffer.from(data) + ) + + fs.writeFile(filePath, encryptedData, (error) => { + if (error) console.log(error); + //console.log(encryptedData); + console.log("File Successfully Encrypted."); + }); + }); +}; + +//It is for encrypting owner's file data with requester's public key -also output is in HEX +exports.getEncryptFileV2 = (pbKey, filePath ) => { + return new Promise( (resolve) => { + + //Store publicKey.key as key file + //Also pass publicKey.key the file here as a parameter + //let pbKey = fs.readFileSync('./keys/publicKey.key'); + let againEncryptedfileP = './temp-file/againEncryptFile.txt'; + console.log("Inside encryptFileV2: "); + console.log("file data: ", fs.readFileSync(filePath).toString('hex')); + fs.readFile(filePath, "utf-8" , (err, data) => { + + + + const encryptedData = crypto.publicEncrypt( + { + key: pbKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: "sha256", + }, + + Buffer.from(data) + ) + + fs.writeFile(againEncryptedfileP, encryptedData.toString('hex'), (error) => { + if (error) console.log(error); + console.log("File Successfully Encrypted."); + resolve(againEncryptedfileP); + }); + //resolve(againEncryptedfileP); + }); + }); +}; + +exports.getDecryptFile = (filePath) => { + return new Promise( (resolve) => { + + const plainDataFileP = './temp-file/decryptedFile.txt'; + //let prKey = fs.readFileSync('./keys/privateKey.key'); //toString?? + fs.readFile('./keys/privateKey.key', 'utf8', (err, prKey)=> { + + console.log('In the getDecryptFile(). '); + console.log(filePath); + + const tempPath = './public/files/'+ filePath; + + fs.readFile(tempPath, (err, encryptedData) => { + + const decryptedData = crypto.privateDecrypt( + { + key: prKey, + passphrase: 'sse', + padding: crypto.constants.RSA_PKCS8_OAEP_PADDING, + oaepHash: "sha256", + }, + encryptedData + ) + fs.writeFile(plainDataFileP, decryptedData , (error) => { + console.log('In the getDecryptFile(). writeFile '); + if (error) console.log(error); + console.log(decryptedData); + console.log("Successfully decrypted."); + resolve(plainDataFileP); + }); + //resolve(plainDataFileP); + }); + }); + }); + //return plainDataFilePath; +}; + +exports.getDecryptFileContent = (fileContent) => { + return new Promise((resolve)=> { + console.log("File Content: "+ fileContent); + fs.readFile('./keys/privateKey.key', 'utf8', (err, prKey) => { + console.log('prKey = ', prKey, typeof(prKey)); + + const plainData = crypto.privateDecrypt( + { + key: prKey, + passphrase: 'sse', + padding: crypto.constants.RSA_PKCS8_OAEP_PADDING, + oaepHash: "sha256", + }, + Buffer.from(fileContent,'hex') + ) + + //console.log("decrypted data: ", keyword.toString()); + resolve(plainData.toString() ); + }) + }); +}; + + +exports.generateNonce = () => { + return new Promise((resolve)=> { + + crypto.randomInt(0, 1000000, (err, n) => { + if (err) throw err; + + resolve(n.toString()); + }) + }); +}; + +exports.getEncryptNonce = (pbKey, plainNonce) => { + return new Promise( (resolve) => { + console.log("At Nonce Encrypt: plainNonce: " + plainNonce +" tpy: "+ typeof(plainNonce)); + + const encryptedNonce = crypto.publicEncrypt( + { + key: pbKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: "sha256", + }, + + Buffer.from(plainNonce) + ) + console.log("At Nonce Encrypt: encryptNonce: ", encryptedNonce.toString('hex') + " typ: " + typeof(encryptedNonce.toString('hex'))); + resolve(encryptedNonce.toString('hex')); + }); +}; + +exports.getDecryptNonce = (encryptedNonce) => { + return new Promise( (resolve) => { + console.log("At Nonce decrypt: encryptedNonce: " + encryptedNonce +" tpy: "+ typeof(encryptedNonce)); + console.log("At Nonce decrypt: encryptedNonce: buffer convert " + Buffer.from(encryptedNonce, 'hex') +" tpy: "+ typeof(Buffer.from(encryptedNonce, 'hex'))); + + fs.readFile('./keys/privateKey.key', 'utf8', (err, prKey)=> { + + const decryptedNonce = crypto.privateDecrypt( + { + key: prKey, + passphrase: 'sse', + padding: crypto.constants.RSA_PKCS8_OAEP_PADDING, + oaepHash: "sha256", + }, + Buffer.from(encryptedNonce, 'hex') //should use buffer??? + ) + console.log("At Nonce decrypt: decryptedNonce: " + decryptedNonce.toString() +" tpy: "+ typeof(decryptedNonce.toString())); + resolve(decryptedNonce.toString()); + }); + }); +} \ No newline at end of file diff --git a/fabcar/javascript/app.js b/fabcar/javascript/app.js new file mode 100644 index 0000000..7c42ac0 --- /dev/null +++ b/fabcar/javascript/app.js @@ -0,0 +1,142 @@ +const express = require('express') +const bodyParser = require('body-parser') +const path = require('path') +const multer = require('multer'); +const mongoose = require('mongoose'); + +const app = express() +const port = 5000; + + +/// +const session = require('express-session'); +const MongoDBStore = require('connect-mongodb-session')(session); +const MONGODB_URI = "mongodb+srv://arnobkumarsaha:sustcse16@cluster0.nj6lk.mongodb.net/searchableEncryption?retryWrites=true&w=majority"; + +const csrf = require('csurf'); // to prevent csrf attack +const flash = require('connect-flash'); // to help users by showing what error occured. + +const User = require('./models/user'); + + +const encDec = require('./EncryptDecrypt-v2'); +const fs = require('fs'); + + +const fileStorage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, 'public/files'); + }, + filename: (req, file, cb) => { + cb(null, new Date().toISOString() + '-*-' + file.originalname); + } +}); + +//encDec.getEncryptFile('rosalind.txt'/* '2021-05-27T18:21:22.710Z-*-solive.txt' */); + +app.set("view engine", "ejs"); +app.use(express.urlencoded({ extended: false })); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); + +app.use(multer({storage: fileStorage}).single('file')); +app.use(express.static(path.join(__dirname, "public"))); + + +// Specifying the session storage & then use the middleware +const store = new MongoDBStore({ + uri: MONGODB_URI, + collection: 'sessions' +}); +const csrfProtection = csrf(); + + +// session, crsf(cross site request forgery) & flash Middlewares +app.use( + session({ + httpOnly: false, + secret: 'my secret', + resave: false, + saveUninitialized: false, + store: store + }) +); +app.use(csrfProtection); +app.use(flash()); + + + +// Check if the requester is authenticated or not +app.use((req, res, next) => { + if (!req.session.user) { + return next(); + } + User.findById(req.session.user._id) + .then(user => { + req.user = user; + next(); + }) + .catch(err => console.log(err)); +}); + +// res.locals variables are passed to every views. +app.use((req, res, next) => { + res.locals.isAuthenticated = req.session.isLoggedIn; + res.locals.csrfToken = req.csrfToken(); + + if(!req.user){ + res.locals.profileName = ""; + } + else res.locals.profileName = req.user.name; + next(); +}); + + + + +// About Routes. +var usersRouter = require("./routes/user"); +var indexRouter = require("./routes/index"); + +const errorController = require('./controllers/error'); + +/* +app.get('',(req,res) =>{ + res.render('index') +}) +app.get('/form',(req,res) =>{ + res.render('form') +})*/ + +/* +app.post('/form', urlencodedParser, (req,res) => { + res.json(req.body) +})*/ + + +app.use("/user", usersRouter); + +app.use("/", indexRouter); + +app.use(errorController.get404); + + +mongoose + .connect( + MONGODB_URI, + { + useNewUrlParser: true, + useUnifiedTopology: true + } + ) + .then(result =>{ + app.listen(port, ()=> console.info("App listening on port: " , port)) + console.log("Yesss ! MongoDb is connected."); + }) + .catch(err =>{ + console.log(err); + }); + + + +// app.listen(port, ()=> console.info("App listening on port: " , port)) \ No newline at end of file diff --git a/fabcar/javascript/bob.txt b/fabcar/javascript/bob.txt new file mode 100644 index 0000000..58200e3 --- /dev/null +++ b/fabcar/javascript/bob.txt @@ -0,0 +1 @@ +Now Bob sends a msg to Alice!!!! diff --git a/fabcar/javascript/controllers/error.js b/fabcar/javascript/controllers/error.js new file mode 100644 index 0000000..376d091 --- /dev/null +++ b/fabcar/javascript/controllers/error.js @@ -0,0 +1,7 @@ +exports.get404 = (req, res, next) => { + res.status(404).render('404', { + pageTitle: 'Page Not Found', + path: '/404' + }); + }; + \ No newline at end of file diff --git a/fabcar/javascript/controllers/index.js b/fabcar/javascript/controllers/index.js new file mode 100644 index 0000000..7b7d04b --- /dev/null +++ b/fabcar/javascript/controllers/index.js @@ -0,0 +1,278 @@ +const bcrypt = require('bcryptjs'); + +const { validationResult } = require('express-validator'); + +const User = require('../models/user'); + +const encDec = require('../EncryptDecrypt-v2'); + +const fs = require('fs'); +const path = require('path'); + +let publicKeyPath = path.join(__dirname, '..', 'keys', 'publicKey.key'); + +const init = require('../util'); + + + +exports.getFrontPage = (req,res, next) => { + res.redirect("/user"); +} + +exports.getLogin = (req,res, next) => { + let message = req.flash('error'); + if (message.length > 0) { // if there is an error-flash + message = message[0]; + } else { + message = null; + } + res.render('auth/login', { + path: '/login', + pageTitle: 'Login Page', + errorMessage: message, + oldInput: { + email: '', + password: '' + }, + validationErrors: [] + }); +} + +function loginHelper(req,res,errors){ + res.status(422).render('auth/login', { + path: '/login', + pageTitle: 'Login', + errorMessage: errors.array()[0].msg, + oldInput: { + email: req.body.email, + password: req.body.password, + typeOfUser: req.body.typeOfUser + }, + validationErrors: errors.array() + }); + }; + +exports.postLogin = (req,res, next) => { + const email = req.body.email; + const password = req.body.password; + + const errors = validationResult(req); + // if there are errors , render the same page (with user-entered info.) + if (!errors.isEmpty()) { + return loginHelper(req,res,errors); + } + + User.findOne({ email: email }) + .then(user => { + // user exists with this email , bcz we checked it already in auth route. + bcrypt + .compare(password, user.password) + .then(doMatch => { + if (doMatch) { // password validation + req.session.isLoggedIn = true; + req.session.user = user; + return req.session.save(err => { + console.log(err); + res.redirect('/'); + }); + } + // Incorrect password entered. + + return res.status(422).render('auth/login', { + path: '/login', + pageTitle: 'Login Page', + errorMessage: 'Invalid email or password', + oldInput: { + email: req.body.email, + password: req.body.password + }, + validationErrors: errors.array() + }); + }) + .catch(err => { + console.log(err); + res.redirect('/login'); + }); + }) + .catch(err => console.log(err)); +} + + +exports.getSignup = (req,res, next) => { + let message = req.flash('error'); + if (message.length > 0) { + message = message[0]; + } else { + message = null; + } + res.render('auth/signup', { + path: '/signup', + pageTitle: 'Signup', + errorMessage: message, + oldInput: { // for better user experience... So that user don't have to enter all the fields again. + email: '', + password: '', + confirmPassword: '', + name: '' + }, + validationErrors: [] + }); +} + +// Make a function in index.js controller. +function fudai(){ + return new Promise((resolve, reject) => { + fs.readFile(publicKeyPath, 'utf8', (err, data)=> { + console.log('Inside fudai.'); + resolve(data); + }); + }); +} + +// postSignup function from two-host branch +/* +exports.postSignup = async (req,res, next) => { + const email = req.body.email; + const password = req.body.password; + const confirmPassword = req.body.confirmPassword; + const name = req.body.name; + + const errors = validationResult(req); + // if there are errors , render the same page (with user-entered info.) + if (!errors.isEmpty()) { + return res.status(422).render('auth/signup', { + path: '/signup', + pageTitle: 'Signup', + errorMessage: errors.array()[0].msg, + oldInput: { + email: email, + password: password, + confirmPassword: confirmPassword, + name: name + }, + validationErrors: errors.array() + }); + } + // If no error, encrypt the password, and save the user into database. + const hashedPassword = await bcrypt.hash(password, 12); + + await encDec.generateKeys(); + + let pbKey = await fudai(); + //let pbKey = await encDec.generateKeys(); + + console.log(pbKey, typeof(pbKey) ); // This is buffer Object + + const user = new User({ + email: email, + password: hashedPassword, + name: name, + cart: { + myFiles: [] + }, + reqs: { + notifications: [] + }, + publicKey: pbKey + }); + + try{ + const result = await user.save(); + res.redirect('/login'); + } + catch(err){ + console.log(err); + }; +} +*/ + + +//FABRIC PART STARTS +exports.postSignup = async (req,res, next) => { + const email = req.body.email; + const password = req.body.password; + const confirmPassword = req.body.confirmPassword; + const name = req.body.name; + + const errors = validationResult(req); + // if there are errors , render the same page (with user-entered info.) + if (!errors.isEmpty()) { + return res.status(422).render('auth/signup', { + path: '/signup', + pageTitle: 'Signup', + errorMessage: errors.array()[0].msg, + oldInput: { + email: email, + password: password, + confirmPassword: confirmPassword, + name: name + }, + validationErrors: errors.array() + }); + } + + // If no error, encrypt the password, and save the user into database. + const hashedPassword = await bcrypt.hash(password, 12); + + await encDec.generateKeys(); + + let pbKey = await fudai(); + //let pbKey = await encDec.generateKeys(); + + console.log(pbKey, typeof(pbKey) ); // This is buffer Object + + const user = new User({ + email: email, + password: hashedPassword, + name: name, + cart: { + myFiles: [] + }, + reqs: { + notifications: [] + }, + publicKey: pbKey + }); + + try{ + const result = await user.save(); + + // ========================================================================================= + // FABRIC PART STARTS + // ========================================================================================= + try { + var contract = (await init).getInput(); + + var ct = contract.contact; + var way = contract.gateway; + // Submit the specified transaction + await ct.submitTransaction( + "Register", //must declare in capital letter in both js and Go + result._id, + email, + name + ); + //await ct.submitTransaction('CreateMed', 'MED12', 'Beximco', 'NapaVX', 'fever,common cold and influenza.'); + + console.log("UserRegister Transaction has been submitted"); + // res.redirect("/login"); + } catch (error) { + console.error(`Failed to submit UserRegister transaction: ${error}`); + process.exit(1); + } + // ========================================================================================= + // FABRIC PART ENDS + // ========================================================================================= + + res.redirect('/login'); + } + catch(err){ + console.log(err); + }; +} +exports.postLogout = (req, res, next) => { + req.session.destroy(err => { + console.log(err); + res.redirect('/'); + }); +}; diff --git a/fabcar/javascript/controllers/userShowReq.js b/fabcar/javascript/controllers/userShowReq.js new file mode 100644 index 0000000..20d485f --- /dev/null +++ b/fabcar/javascript/controllers/userShowReq.js @@ -0,0 +1,417 @@ +const User = require('../models/user'); +const KeywordIndex = require('../models/keyword_index'); +const File = require('../models/file'); + +const path = require('path'); +const fs = require('fs'); +const encDec = require('../EncryptDecrypt-v2'); + +const { ObjectID } = require('bson'); +const documents = require('../controllers/userUpDownDel').getDocuments(); + + +const init = require('../util'); + +exports.showFileById = (req, res, next) =>{ + const fileId = req.params.myFileId; + + console.log("showFileById : fileId = ", fileId); + + File.findById(ObjectID(fileId)).then((theFile) => { + const filePath = theFile.filePath; + let content; + + fs.readFile(path.dirname(process.mainModule.filename) + '/public/files/'+filePath, 'utf8' , (err, data) => { + if (err) { + console.error(err); + return; + } + console.log(data); + + res.render("updown/showFile", { + pageTitle: "Show File", + path: "/user/uploaded", + fileContent: data + }); + }); + }) +} + +exports.requestFile = async (req, res, next) =>{ + const ownerId = req.params.ownerId; + const fileName = req.params.fileName; // the file path actually (without /public/files.) + console.log("requesting file begins"); + + + + //If it is his file. + if(req.user._id == ownerId){ + return res.render('updown/searchResult', { + pageTitle: "Search result", + path: "/user/searchResult", + docs: documents, + errorMessage: "This is your file man! " + }); + } + + + const theFile = await File.findOne({filePath: fileName}); + let owner = await User.findOne({_id: ownerId}); + + + + + //here is pushing nonce --starts + let nonce = await encDec.generateNonce(); //use random number or string -- but output must be in string + const updatedRequestedItems = [...req.user.dcart.allRequests]; + updatedRequestedItems.push({ + isAccept: 0, + ownerId: ownerId, + requestedFileId: theFile._id, + noncePlain: encDec.getKeywordHash(nonce), + fileContent: null, + nonceGet: null + }); + const updatedAllReqs = { + allRequests: updatedRequestedItems + }; + //console.log(updatedAllReqs); + req.user.dcart = updatedAllReqs; + await req.user.save(); + //here is pushing nonce --ends + + + + // Update the Owner's notification array. + const updatedNotificationItems = [...owner.reqs.notifications]; + console.log("theFile = ", theFile, "owner = ", owner, "updatedNoti = ", updatedNotificationItems); + updatedNotificationItems.push({ + requesterId: req.user._id, + requestedFileId: theFile._id, + decided: false, + nonce: await encDec.getEncryptNonce(owner.publicKey, nonce) + }); + const updatedReqs = { + notifications: updatedNotificationItems + }; + console.log("updated noti = ", updatedNotificationItems, "updated reqs = ", updatedReqs); + owner.reqs = updatedReqs; + owner = await owner.save(); + + console.log('Done with reques file.'); + + + // ========================================================================================= + // FABRIC PART STARTS + // ========================================================================================= + try { + var contract = (await init).getInput(); + + var ct = contract.contact; + var way = contract.gateway; + // Submit the specified transaction + await ct.submitTransaction( + "FileRequestNotification", //must declare in capital letter in both js and Go + theFile._id, + owner._id, + req.user._id, + 'queued' + ); + //await ct.submitTransaction('CreateMed', 'MED12', 'Beximco', 'NapaVX', 'fever,common cold and influenza.'); + + console.log(" Request-for-file-transaction has been submitted"); + // res.redirect("/login"); + } catch (error) { + console.error(`Failed to submit Request-for-file-transaction: ${error}`); + process.exit(1); + } + // ========================================================================================= + // FABRIC PART ENDS + // ========================================================================================= + + + + // Show the search results to the User. + res.render('updown/searchResult', { + pageTitle: "Search result", + path: "/user/searchResult", + docs: documents, + errorMessage: "Requset has been sent to the DataOwner. " + }); + } + +function fudai(p){ + return new Promise((resolve, reject) => { + fs.readFile(p, (err, data)=> { //can not be utf8 + console.log('Inside fudai.. data:'+ data.toString()); + resolve(data); + }); + }); +} + +function findNonce(notifications,requesterId,requestedFileId){ + return new Promise((resolve) => { + for(let item of notifications){ + console.log('item -> ', item); + if(item.requesterId.equals(requesterId) && item.requestedFileId.equals(requestedFileId) && item.decided == false){ + console.log('Nonce -> ', item.nonce); + resolve(item.nonce); + } + } + }); +} + +exports.grantPermission = async (req, res, next) =>{ + const requesterId = req.params.requesterId; + const requestedFileId = req.params.requestedFileId; + const theFile = await File.findOne({_id: requestedFileId}); + const requester = await User.findOne({_id: requesterId}); + console.log("theFile = ", theFile, "requester = ", requester, "requester public key = ", requester.publicKey); + + + + + //const plainDataFilePath = await encDec.getDecryptFile(theFile.filePath); + //await encDec.getEncryptFileV2(requester.publicKey.toString(), plainDataFilePath); + const plainDataFilePath = await encDec.getDecryptFile(theFile.filePath); + const fpath = await encDec.getEncryptFileV2(requester.publicKey, plainDataFilePath); + + + + // ========================================================================================= + // FABRIC PART STARTS + // ========================================================================================= + try { + var contract = (await init).getInput(); + + var ct = contract.contact; + var way = contract.gateway; + // Submit the specified transaction + await ct.submitTransaction( + "FileRequestAccept", //must declare in capital letter in both js and Go + theFile._id, + req.user._id, + requester._id, + 'accepted' + ); + //await ct.submitTransaction('CreateMed', 'MED12', 'Beximco', 'NapaVX', 'fever,common cold and influenza.'); + + console.log(" Accept-request-transaction has been submitted"); + // res.redirect("/login"); + } catch (error) { + console.error(`Failed to submit Accept-request-transaction: ${error}`); + process.exit(1); + } + // ========================================================================================= + // FABRIC PART ENDS + // ========================================================================================= + + + + + //let encryptedContent = await fudai(fpath); + + //for nonce + //just for test: + //let N = await encDec.getDecryptNonce(await encDec.getEncryptNonce(req.user.publicKey, "122")); + //console.log("Nonce Testing: " + N); + + let currentUser = await User.findOne({_id: req.user._id}); + const notifications = [...currentUser.reqs.notifications]; + const nonce = await findNonce(notifications, requesterId, requestedFileId); + + let againEncryptedNonce = await encDec.getEncryptNonce(requester.publicKey, await encDec.getDecryptNonce(nonce)); + + //object already EXISTS......Dont push UPDATE::::STARTS + const queryRequest = { + 'dcart.allRequests': { + $elemMatch: { + isAccept: 0, + ownerId: req.user._id, + requestedFileId: requestedFileId + } + } + }; + console.log("Result for queryRequest: ", await User.findOne(queryRequest)); + await User.update(queryRequest, {$set: { + 'dcart.allRequests.$.isAccept': 2, + 'dcart.allRequests.$.fileContent': await fudai(fpath), + 'dcart.allRequests.$.nonceGet': againEncryptedNonce + }} ); +//object already EXISTS......Dont push UPDATE::::ENDS + + + +//cmnt starts here for nonce addinf purpose +/* const updatedRequestedItems = [...requester.dcart.allRequests]; + + updatedRequestedItems.push({ + isAccept: 2, + ownerId: req.user._id, + requestedFileId: theFile._id, + nonceSent: againEncryptedNonce, + fileContent: await fudai(fpath) //encryptedContent + }); + const updatedAllReqs = { + allRequests: updatedRequestedItems + }; + //console.log(updatedAllReqs); + requester.dcart = updatedAllReqs; + await requester.save(); + */ +//cmt ends here --- for nonce adding purpuse + + console.log(requester); + + //need to change + + /* + let currentUser = await User.findOne({_id: req.user._id}); + const updatedNotificationItems = [...currentUser.reqs.notifications]; + let itemToBeSaved; + for(let item of updatedNotificationItems){ + console.log('item -> ', item); + if(item.requesterId.equals(requesterId) && item.requestedFileId.equals(requestedFileId)){ + item.decided = true; + itemToBeSaved = item; + break; + } + } + */ + + const query = { + 'reqs.notifications': { + $elemMatch: { + requesterId: requesterId, + requestedFileId: requestedFileId + } + } + }; + + + await User.updateOne(query, {$set: { + 'reqs.notifications.$.decided': true + }} ); + + console.log('Done with Grant Permission.'); + res.redirect('/user/notification'); +} + +exports.denyPermission = async (req, res, next) =>{ + const requesterId = req.params.requesterId; + const requestedFileId = req.params.requestedFileId; + + const requester = await User.findOne({_id: requesterId}); + + const updatedRequestedItems = [...requester.dcart.allRequests]; + + //for nonce + let currentUser = await User.findOne({_id: req.user._id}); + const notifications = [...currentUser.reqs.notifications]; + const nonce = await findNonce(notifications, requesterId, requestedFileId); + + let againEncryptedNonce = await encDec.getEncryptNonce(requester.publicKey, await encDec.getDecryptNonce(nonce)); + + //object already EXISTS......Dont push UPDATE::::STARTS + const queryRequest = { + 'dcart.allRequests': { + $elemMatch: { + isAccept: 0, + ownerId: req.user._id, + requestedFileId: requestedFileId + } + } + }; + console.log("Result for queryRequest: ", await User.findOne(queryRequest)); + await User.update(queryRequest, {$set: { + 'dcart.allRequests.$.isAccept': 1, + 'dcart.allRequests.$.nonceGet': againEncryptedNonce + }} ); + //object already EXISTS......Dont push UPDATE::::ENDS + + // For using Nonce - we dont push already added object - we update it -- start + //console.log(updatedRequestedItems); +/* updatedRequestedItems.push({ + isAccept: 1, + ownerId: req.user._id, + nonceSent: againEncryptedNonce + }); + const updatedAllReqs = { + allRequests: updatedRequestedItems + }; + + requester.dcart = updatedAllReqs; + await requester.save(); */ + // For using Nonce - we dont push already added object - we update it -- end + + console.log('\n', requester, '\n'); + + //need to change + const query = { + 'reqs.notifications': { + $elemMatch: { + requesterId: requesterId, + requestedFileId: requestedFileId + } + } + }; + await User.updateOne(query, {$set: { + 'reqs.notifications.$.decided': true + }} ); + + console.log('Done with Deny Permission.'); + res.redirect('/user/notification'); +} + +exports.getAllNotifications = (req, res, next) =>{ + User.findOne({_id: req.user._id}) + .then(user =>{ + const notifications = user.reqs.notifications; + res.render("updown/notification",{ + pageTitle: "Notifications", + path: "/user/notification", + notifications: notifications + }) + }) +} + +exports.getAllRequests = (req, res, next) =>{ + User.findOne({_id: req.user._id}) + .then(user =>{ + const requests = user.dcart.allRequests; + res.render("updown/request",{ + pageTitle: "All Requests", + path: "/user/request", + requests: requests + }) + }) +} + +function verifyNonce(noncePlain, plainNonce){ + let verify = false; + if(JSON.stringify(noncePlain) == JSON.stringify(encDec.getKeywordHash(plainNonce))){ + verify = true; + } + return verify; +} + +exports.showDecryptedFileContent = async(req, res, next) =>{ + const content = req.params.fileContent; + const noncePlain = req.params.noncePlain; + const nonceGet = req.params.nonceGet; + //console.log(fs.readFileSync('./public/files/rosalind.txt').toString()); + //console.log(nonceGet); + + let plainNonce = await encDec.getDecryptNonce(nonceGet); + let verify = verifyNonce(noncePlain, plainNonce); + console.log(verify); + + const plainData = await encDec.getDecryptFileContent(content); + + res.render("updown/showDecryptedContent",{ + pageTitle: "File Content", + path: "/user/request", + documents: plainData, + verification: verify + }) +} \ No newline at end of file diff --git a/fabcar/javascript/controllers/userUpDownDel.js b/fabcar/javascript/controllers/userUpDownDel.js new file mode 100644 index 0000000..b19d95d --- /dev/null +++ b/fabcar/javascript/controllers/userUpDownDel.js @@ -0,0 +1,393 @@ +const User = require('../models/user'); +const KeywordIndex = require('../models/keyword_index'); +const File = require('../models/file'); + +const path = require('path'); +const fs = require('fs'); +const encDec = require('../EncryptDecrypt-v2'); +const { ObjectID } = require('bson'); + +const init = require('../util'); + +exports.getFrontPage = (req, res, next) => { + res.render("users", { + pageTitle: "User", + path: "/user", + editing: false, + }); +}; + + + +// About Upload file .............................................................. + +exports.getUploadFile = (req, res, next) =>{ + res.render("updown/upload", { + pageTitle: "Upload a file", + path: "/user/upload", + errorMessage: "" + }); +} + +var matchCount = 0; +var bestMatchFiles = {}; + +exports.postUploadFile = async (req, res, next) =>{ + const name = req.body.name; + const keyword = req.body.keyword; + const file = req.file; + let tempPath = file.path; + + + // encrypt the file, store it to the same path + encDec.getEncryptFile(req.user.publicKey, tempPath); + tempPath = file.path.split('/')[2]; + // this is to store in the user.cart.myFiles + console.log("Here in PostUplod public key of owner: " + req.user.publicKey); + let encryptedKeyword = encDec.getEncryptionKeyword(req.user.publicKey, keyword); + let space_separated_keywords = keyword.split(' '); + + + + const numberOfKeyword = space_separated_keywords.length; + let HashedKeywordSet = []; + matchCount = 0; + // In this for loop, I have just counted the files-frequency (those are related with keywords) in bestMatchFiles container. + for(let tmpKey of space_separated_keywords){ + const keyHash = encDec.getKeywordHash(tmpKey); + HashedKeywordSet.push(keyHash); + const keyDoc = await KeywordIndex.findOne({index_hash:keyHash}); + if(keyDoc){ + // which files contain this key .. + let myfiles = [...keyDoc.whereItIs.myFiles]; + for(var fl of myfiles){ + var f = fl.filePath; + if(bestMatchFiles[f]>=1) {bestMatchFiles[f]++;} + else {bestMatchFiles[f]=1;} + } + } + } + + + // which file has most common keyword set with the currently uploaded file. + for (let [key,value] of Object.entries(bestMatchFiles) ){ + //console.log('printing :', key, value); + matchCount = Math.max(value, matchCount); + } + let decide = false; + for (let [key,value] of Object.entries(bestMatchFiles) ){ + if(matchCount == value){ + const matchedFile = await File.findOne({filePath: key}); + if(matchedFile && matchedFile.numberOfKeyword == HashedKeywordSet.length){ + decide = true; + } + } + } + + + if(matchCount == HashedKeywordSet.length && decide){ + //keyword string fully matched + return res.status(422).render('updown/upload', { + path: '/upload', + pageTitle: 'Upload a file', + errorMessage: "Keyword fully Matched!!!", + oldInput: { + name: name, + keyword: keyword, + file: file + } + }); + } + + + + // Code is here ... That means all keywords didn't matched. + // In this for loop, Just saving or updating the KewordIndex table. + //*****************************Upper part is about checking, Lower part is about saving/updating******************************* + + + // Part 1: Just create a new File + let newSavedFile = new File({ + filePath: tempPath, + keyword: encryptedKeyword, + numberOfKeyword: numberOfKeyword, + store: { + keywordsList: [] + } + }); + newSavedFile = await newSavedFile.save(); + //console.log("\n newSavedFile1 = ", newSavedFile,"\n"); + + + + + // Part 2: In this for loop, Just saving or updating the KewordIndex table. + let keyList = [] + + for(let keyHash of HashedKeywordSet){ + //const keyDoc = await KeywordIndex.findOne({index_hash: keyHash}); + let keyDoc = await KeywordIndex.findOne({index_hash: keyHash}); + if(keyDoc){ + // If the index is already there. Just update the keyDoc.whereItIs.myFiles array. + let newMyFiles = [...keyDoc.whereItIs.myFiles]; + newMyFiles.push({ + //filePath: tempPath + fileId: newSavedFile._id + }); + + const updatedList = { + myFiles: newMyFiles + }; + keyDoc.whereItIs = updatedList; + keyDoc.save(); + } + else{ + // If the index is not in the db, then create a new one. + let newKey = new KeywordIndex({ + index_hash: keyHash, + whereItIs: { + //myFiles: [ {filePath: tempPath} ] + myFiles: [ {fileId: newSavedFile._id} ] + } + }); + //newKey.save(); + newKey = await newKey.save(); + keyDoc = newKey; + } + keyList.push({keyHashId: keyDoc._id}); + } + //console.log("\n keyList = ", keyList, "\n"); + + + + // Part 3: Updating the Files's keyword list. + const updatedStore = { + keywordsList: keyList + }; + //console.log("updatedStore = ", updatedStore); + + const toCheckList = [...newSavedFile.store.keywordsList]; + + //console.log("ToCheckList 1 = ", toCheckList); + + for(let obj of keyList){ + toCheckList.push(obj); + } + + //console.log("ToCheckList 2 = ", toCheckList); + + //console.log("newSavedFile.store = ", newSavedFile.store); + newSavedFile.store = updatedStore; + //console.log("newSavedFile.store = ", newSavedFile.store); + //console.log("\n newSavedFile2 = ", newSavedFile,"\n"); + + newSavedFile = await newSavedFile.save(); + //console.log("\n newSavedFile3 = ", newSavedFile,"\n"); + + + // ========================================================================================= + // FABRIC PART STARTS + // ========================================================================================= + try { + var contract = (await init).getInput(); + + var ct = contract.contact; + var way = contract.gateway; + // Submit the specified transaction + await ct.submitTransaction( + "FileUpload", //must declare in capital letter in both js and Go + newSavedFile._id, + name, + tempPath, + encryptedKeyword, + numberOfKeyword + ); + //await ct.submitTransaction('CreateMed', 'MED12', 'Beximco', 'NapaVX', 'fever,common cold and influenza.'); + + console.log("FileUplaod Transaction has been submitted"); + // res.redirect("/login"); + } catch (error) { + console.error(`Failed to submit FileUpload transaction: ${error}`); + process.exit(1); + } + // ========================================================================================= + // FABRIC PART ENDS + // ========================================================================================= + + + +// Part 4: Updating to User table. + return req.user.addToCart(newSavedFile._id) + .then(result=>{ + res.redirect('/user/uploaded'); + }); + +}; + +// About Download file .................................................................. + +exports.getDownloadFile = (req, res, next) =>{ + res.render("updown/download", { + pageTitle: "Download a file", + path: "/user/download" + }); +} + +// It finds out the entries from DB those are matched with searched keywords. +// Then finds out the file path, And count the frequencies. +var freqTable = {}; +async function calculate1(space_separated_keywords){ + + freqTable = {}; + + for(let tmpKey of space_separated_keywords){ + const keyHash = await encDec.getKeywordHash(tmpKey); + + const keyDoc = await KeywordIndex.findOne({index_hash: keyHash}); + if(keyDoc){ + let myfiles = [...keyDoc.whereItIs.myFiles]; + + for(let fl of myfiles){ + + let temp = await File.findOne({_id: fl.fileId}); + let fp = temp.filePath; + if(freqTable[fp]==1) {freqTable[fp]++;} + else { freqTable[fp] = 1;} + } + + } + else{ + console.log("Inside else block!"); + } + } +} + +let sortable = []; +let documents = []; +// It makes the array sorted in descending order. To show more matched files before the less matched files. +function calculate2(){ + sortable = []; + documents = []; + return new Promise( (resolve, reject) =>{ + + for (var file in freqTable) { + sortable.push([file, freqTable[file]]); + } + sortable.sort(function(a, b) { + return b[1] - a[1]; + }); + + resolve(sortable); + }); +} + +async function intermediateFunction(space_separated_keywords){ + console.log("At the start of intermediate function."); + await calculate1(space_separated_keywords); + await calculate2(); + + console.log('sortable = ', sortable); + + for(let doc of sortable){ + //const userDoc = await User.findOne({ "cart.myFiles.filePath": doc[0]}, {name: 1} ); + const fileDoc = await File.findOne({ filePath: doc[0]}); + console.log(fileDoc._id); + const userDoc = await User.findOne({ "cart.myFiles.myFileId": fileDoc._id} ); + console.log("useDoc = ", userDoc); + doc.push(userDoc._id); + documents.push(doc); + } + return; +} + +exports.postDownloadFile = async (req, res, next) =>{ + + const keyword = req.body.keyword; + + let space_separated_keywords = keyword.split(' '); + + await intermediateFunction(space_separated_keywords); + + res.render('updown/searchResult', { + pageTitle: "Search result", + path: "/user/searchResult", + docs: documents, + errorMessage: "" + }); +} + + +// Rendering Uploaded and downloaded files................................................... + +exports.getUploadedFiles = (req, res, next) =>{ + User.findOne({_id: req.user._id}) + .then(user =>{ + const files = user.cart.myFiles; + res.render("updown/uploaded",{ + pageTitle: "Uploaded Files", + path: "/user/uploaded", + files: files + }) + }) +} +exports.getDownloadedFiles = (req, res, next) =>{ + res.render("updown/downloaded", { + pageTitle: "Downloaded Files", + path: "/user/downloaded" + }); +} + + + + +//Delete part........................................................ + +exports.deleteFile = async (req, res, next) => { + const fileId = req.params.myFileId; + + const theFile = await File.findOne({_id: fileId}); + + //Going through the keyword list of this file, and update them accordingly. + for(let hashId of theFile.store.keywordsList){ + //console.log("_id & hashId from file.store are = ", hashId); + let keyHash = await KeywordIndex.findOne({_id: hashId.keyHashId}); + + //console.log("\n keyHash = ", keyHash, "\n"); + + let updatedList = []; + for(let something of keyHash.whereItIs.myFiles){ + if(something.fileId.equals(theFile._id) ){ + //console.log('\n If statement\n'); + } + else{ + updatedList.push(something); + } + } + console.log(updatedList); + const updatedWhereItIs = { + myFiles: updatedList + } + keyHash.whereItIs = updatedWhereItIs; + keyHash = await keyHash.save(); + + if(updatedList.length == 0){ + await KeywordIndex.deleteOne({_id: hashId.keyHashId}); + } + } + + // Now, delete from fileSystem and then the file itself + fs.unlinkSync(path.resolve('./') + '/public/files/' + theFile.filePath); + + await File.deleteOne({_id: theFile._id}); + console.log('successfully deleted !'); + + return req.user.deleteFromCart(fileId) + .then(result =>{ + res.redirect('/user/uploaded'); + }); +} + + +exports.getDocuments = function(){ + return documents; +} \ No newline at end of file diff --git a/fabcar/javascript/database.js b/fabcar/javascript/database.js new file mode 100644 index 0000000..4b9d7c4 --- /dev/null +++ b/fabcar/javascript/database.js @@ -0,0 +1,28 @@ +const mongodb = require("mongodb"); +const MongoClient = mongodb.MongoClient; + +let _db; + +const mongoConnect = (callback) =>{ + + MongoClient.connect( + 'mongodb+srv://arnobkumarsaha:sustcse16@cluster0.nj6lk.mongodb.net/searchableEncryption?retryWrites=true&w=majority' + ).then( (client)=>{ + console.log("MongoDB connected !"); + _db = client.db(); + callback(); + }).catch( (err)=>{ + console.log(err); + throw err; + }); +} + +const getDb = () =>{ + if(_db){ + return _db; + } + throw 'No Database found.' +} + +exports.mongoConnect = mongoConnect; +exports.getDb = getDb; \ No newline at end of file diff --git a/fabcar/javascript/encryption-decryption.js b/fabcar/javascript/encryption-decryption.js new file mode 100644 index 0000000..9368e64 --- /dev/null +++ b/fabcar/javascript/encryption-decryption.js @@ -0,0 +1,145 @@ +const crypto = require("crypto") +const fs = require('fs'); + +// first we need two keys here - index key and private key. Private key will +// be generated only once and then somehow store it + +indexKey = "9uZlGXa0o64kdbQ6Gb96qw==" +privateKey = "vOVH6sdmpNWjRRIqCc7rdxs01lwHzfr3" + +//here we are search only by keywords section of a file. +// suppose a keyword value for a file is this - + +//keyword = "office management file" + +// for index of keyword in database table - we store the HMAC value +// Like this + +//keyword_hash = crypto.createHmac('sha256', indexKey) +// .update(keyword) +// .digest('hex') + +// we store this hash value in keyword_index column in database table + +//Now for file and other thing storation with encryption +//skip file for this tym - will surely add later + +//suppose we will store keyword and username as encrypted +//for this + +//username = "getThisFromSignUpForm" + +// Now we use AES for this + +const algorithm = 'aes-256-cbc'; +//const key = crypto.randomBytes(32); +//const iv = crypto.randomBytes(16); + +//let cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv); + +//let encryptedUserName = cipher.update(username); +//let encryptedKeyword = cipher.update(keyword); + +// Now store it to the database +// works almost same for file + +exports.getKeywordHash = (keyword) => { + keyword_hash = crypto.createHmac('sha256', indexKey) + .update(keyword) + .digest('hex') + return keyword_hash + }; + +exports.getEncryptionKeyword = (keyword) => { + const iv = crypto.randomBytes(16); + var ivstring = iv.toString('hex').slice(0, 16); + + let cipher = crypto.createCipheriv(algorithm, privateKey, ivstring); + + let encryptedKeyword = cipher.update(keyword, 'utf8', 'hex'); + encryptedKeyword += cipher.final('hex'); + + //let encryptedKeywordIV = hex_to_ascii(ivstring/* iv.toString('hex') */).concat("//").concat(hex_to_ascii(encryptedKeyword)); + let encryptedKeywordIV = ivstring.concat("//").concat(encryptedKeyword); + + return encryptedKeywordIV; +}; + +exports.getDecryptionKeyword = (keyword) => { + let splitString = keyword.split("//"); + let extractIV = splitString[0]; + let extractKeyword = splitString[1]; + + let decipher = crypto.createDecipheriv(algorithm, privateKey, extractIV); + + let decryptedKeyword = decipher.update(extractKeyword, 'hex', 'utf-8') + decryptedKeyword += decipher.final('utf8'); + + return decryptedKeyword; +}; + +exports.getEncryptFile = (filePath) => { + const iv = crypto.randomBytes(16); + var ivstring = iv.toString('hex').slice(0, 16); + + let cipher = crypto.createCipheriv(algorithm, privateKey, ivstring); + + fs.readFile(filePath, "utf-8", (err, data) => { + //console.log(data); + encryptdata = cipher.update(data, 'utf8', 'hex'); + encryptdata += cipher.final('hex'); + + let encryptedDataIV = ivstring.concat("//").concat(encryptdata); + fs.writeFile(filePath, encryptedDataIV, (err) => { + if (err) console.log(err); + console.log(encryptedDataIV); + console.log("Successfully Encrypted."); + }); + }); +}; + +exports.getDecryptFile = (filePath) => { + + fs.readFile(filePath, "binary", (err, data) => { + let splitData = data.split("//"); + let extractIV = splitData[0]; + let extractFileData = splitData[1]; + + const decipher = crypto.createDecipheriv(algorithm, privateKey, extractIV); + + let decrypted = decipher.update(extractFileData, 'hex', 'utf-8') + decrypted += decipher.final('utf8'); + //decoded = decipher.update(data, 'binary', 'binary'); + //decoded += decipher.final('binary'); + //var plaintext = new Buffer(decrypted, 'binary').toString('utf8'); + fs.writeFile(filePath, decrypted , (err) => { + if (err) console.log(err); + console.log(decrypted); + console.log("Successfully decrypted."); + }); + }); + + +}; + +function hex_to_ascii(str1) + { + var hex = str1.toString(); + var str = ''; + for (var n = 0; n < hex.length; n += 2) { + str += String.fromCharCode(parseInt(hex.substr(n, 2), 16)); + } + return str; + } + + function ascii_to_hex(str) + { + var arr1 = []; + for (var n = 0, l = str.length; n < l; n ++) + { + var hex = Number(str.charCodeAt(n)).toString(16); + arr1.push(hex); + } + return arr1.join(''); + } + diff --git a/fabcar/javascript/enrollAdmin.js b/fabcar/javascript/enrollAdmin.js new file mode 100644 index 0000000..49b710a --- /dev/null +++ b/fabcar/javascript/enrollAdmin.js @@ -0,0 +1,56 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const FabricCAServices = require('fabric-ca-client'); +const { Wallets } = require('fabric-network'); +const fs = require('fs'); +const path = require('path'); + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new CA client for interacting with the CA. + const caInfo = ccp.certificateAuthorities['ca.org1.example.com']; + const caTLSCACerts = caInfo.tlsCACerts.pem; + const ca = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the admin user. + const identity = await wallet.get('admin'); + if (identity) { + console.log('An identity for the admin user "admin" already exists in the wallet'); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + const enrollment = await ca.enroll({ enrollmentID: 'admin', enrollmentSecret: 'adminpw' }); + const x509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org1MSP', + type: 'X.509', + }; + await wallet.put('admin', x509Identity); + console.log('Successfully enrolled admin user "admin" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to enroll admin user "admin": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/javascript/invoke.js b/fabcar/javascript/invoke.js new file mode 100644 index 0000000..228aceb --- /dev/null +++ b/fabcar/javascript/invoke.js @@ -0,0 +1,57 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Gateway, Wallets } = require('fabric-network'); +const fs = require('fs'); +const path = require('path'); + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json'); + let ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const identity = await wallet.get('appUser'); + if (!identity) { + console.log('An identity for the user "appUser" does not exist in the wallet'); + console.log('Run the registerUser.js application before retrying'); + return; + } + + // Create a new gateway for connecting to our peer node. + const gateway = new Gateway(); + await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } }); + + // Get the network (channel) our contract is deployed to. + const network = await gateway.getNetwork('mychannel'); + + // Get the contract from the network. + const contract = network.getContract('fabcar'); + + // Submit the specified transaction. + // createCar transaction - requires 5 argument, ex: ('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom') + // changeCarOwner transaction - requires 2 args , ex: ('changeCarOwner', 'CAR12', 'Dave') + await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom'); + console.log('Transaction has been submitted'); + + // Disconnect from the gateway. + await gateway.disconnect(); + + } catch (error) { + console.error(`Failed to submit transaction: ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/javascript/keys/privateKey.key b/fabcar/javascript/keys/privateKey.key new file mode 100644 index 0000000..58ed6f9 --- /dev/null +++ b/fabcar/javascript/keys/privateKey.key @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI8ydU0zxN+9gCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEWBBDy15KaHXXd2VDVtMoC+GvNBIIE +0FysCfVgrtbSsPZpPgrq40LYJAZkUMcdvNsoGB7knbq5zCiLgveD9OgQ/vtL8Mqt +MxG9dfgoWBI7SNVm8/tij3ZiN1jYDgG4VAnnJ5de8iANVaRgYXU46aNp38MtHKo/ +CWpZ0wKkqSBlLWMBXPAl31Ll/trGjgoruvJCinAmjnCwUgQuTlfnJPEDXPauv3WV +mu7LWxyF2rYg9TZC/uQqIezKR0oud5jUeM5xotQvMStLa+qUoE/Frfx6hktu99Qu +l4hLsmyGP+A8SdVMWwf0APwtpv6ujQVBnZXu/xn+oa+AKOyQxr2/ko2ZAbzbc+1j +qSnKIuVjaEs63BmGACxIpQ+VTigaUWKm7xVpfDrpnzZLCJZRfJREUlraARfnuSMQ +IB2lXIKGeqX2gYOWZtUlzpUIwtTit4ARJ60sP2jGgoDhL1fbhAv5VpsktyGvwmla +yxgtC7i95G3jAtnumyTlq3hOyZ0psGYeQQXxcMmwbT2ILM6Q1q6RQF/npak6pyZg +0EgYVOXEO+gE324skI+3Coc01jH2YBE7+QQSazcTWBMIOIWS0k6lsIxj4xzIoeop +h0s+5vvUfY1gq9zfWdG0sRZn3dJ9UwAl0xZhBauIX/76/Fu6UT2nw9N1lHIh3y1H +16Kor1+cvnLB3sA1U4pX7s9hIpE96K6yXbtTfWYflBEKfY71ld2qHizYfhMjzght +lfEo/mvKGY8nqn8O2dM94aq8uGojrWAF+4qtAoySQl3NAhZCxYcyjwf00/hVdj5V +SDA13y5umHfrfuUBCAZ7kKY5Md0aGfg6yV9maWRiTxFtW8cfPj+WuIE7D865Ixcu +kZR0UkxUBlQcnw7+PULZPly3PNNl0UaJzIRsK4gutWhTCy+xvEgOejTiD/3oCGzR +wu1QxnPg0utZ414KDeGxdgS9fh08VxDR6lvo+7wxW1KzSn6jq3UNTbsn1wSzBxwT +rDFVaoKIv+VY1omQXs8ujS41WomVOrlA0WBE2pDUqrsboPU1EfHu8f5FgFN2AeXG +XtJwPr2UqCETO/Q/xkFhyg176z8INVqKnu1LpgRe5zaqzRSujMHskArjJEwHumxs +MyXb6mb+fpeN9VZpLP3RuwdGQk5uFRkiTpDL1wRg5NHOdDOZG1GWTHZW704Q5sIA +LEfFd/SI9fVEHLv/Z3WajQyUVHifJ1DrrrrxmvCznN1GdtRPBmnDvPvs8uNkuDCx +tcPXQfj+brrQ4MdCxJYC2h8xBgsfbeZ53TkyGmx2zM9IzzVAoc6rJE6Msj6f3+E3 +pr7vXOMots+6CFRN92ZD/iZKDT5jKuW2q139Uqg7aLo2bLYqvcTis+uZ5YFAprTb +6MwfnjDWBvccQe4yyjJZO7SqMcQTCckqG+kHnpKD+32kifmdLtvtWayf0KiHJRd/ +UCdiECMIr6F25K8BqeeDelAhKmE0yDkL7X4gjeK06UHQz0Eqqql0WVLnicVsQfOI +ZKq+bttgdq+hyss4h5RVwfi3gQBlPiRTtEaMB0Vau9OCTdUHV8LEh+U0IcAH6jPb +wZE6rJuUOwAN3SaftK71NPH0LkT88RwL3K5Qs83E08qawgKCWfCXm79e3YAldqdV ++z/g0/0eI8wKn8T+zm2tC7HAQ7N+LNrb6GlC+mNC9ayI +-----END ENCRYPTED PRIVATE KEY----- diff --git a/fabcar/javascript/keys/publicKey.key b/fabcar/javascript/keys/publicKey.key new file mode 100644 index 0000000..dc66806 --- /dev/null +++ b/fabcar/javascript/keys/publicKey.key @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEA6k2cS7q7qPmrN1CghuaejHb0HvfMSMDPeVs444s8Yf5BLjva5OgF +mDm6BStnxwWQeg7iokSfGDsEWx7J87tGN3ruALjfUSXSQlQ9Z/wsQ+AmHnrD+l3M +Vi3R9663hOEkAVVWAFIJfvyH/TdALzXhMsexryxXopv6fEqcFUqVLotycSqcutVd +KAcoaku4KOmvIV0z95zSnKpgz7bG6PggnZ+zOsK5HHv+ctsNDB6i4pFio2xcmr+G +QNW7gMcZ0/zkkb4Swo+B66Zk3xuLXb+30DrBsdSx38PtvFdgXRf4ge1EnNLuargb +T1RxOfxwSp1SxD2oh24SE/NkGYGtH5jUoQIDAQEB +-----END RSA PUBLIC KEY----- diff --git a/fabcar/javascript/middleware/is-auth.js b/fabcar/javascript/middleware/is-auth.js new file mode 100644 index 0000000..50f0afb --- /dev/null +++ b/fabcar/javascript/middleware/is-auth.js @@ -0,0 +1,7 @@ + +module.exports = (req, res, next) => { + if (!req.session.isLoggedIn) { // isLoggedIn was set on authController/postLogin route. + return res.redirect('/login'); // If No loggedIn user found , then promt user to log in first. + } + next(); +} \ No newline at end of file diff --git a/fabcar/javascript/models/file.js b/fabcar/javascript/models/file.js new file mode 100644 index 0000000..ea70df2 --- /dev/null +++ b/fabcar/javascript/models/file.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + + +const fileSchema = new Schema({ + filePath: { + type: String, + required: true + }, + store: { + keywordsList: [ + { + keyHashId: {type: Schema.Types.ObjectId, ref: 'Keyword-Index', required: true} + } + ] + }, + keyword: { + type: String, + required: true + }, + numberOfKeyword: { + type: Number, + required: true + } +}); + + +module.exports = mongoose.model('File', fileSchema); \ No newline at end of file diff --git a/fabcar/javascript/models/keyword_index.js b/fabcar/javascript/models/keyword_index.js new file mode 100644 index 0000000..972707f --- /dev/null +++ b/fabcar/javascript/models/keyword_index.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const indexSchema = new Schema({ + index_hash: { + type: String, + required: true + }, + whereItIs: { + myFiles: [ + { + fileId: { type: Schema.Types.ObjectId, ref: 'File', required: true } + } + ] + } +}); + +module.exports = mongoose.model('Keyword-Index', indexSchema); \ No newline at end of file diff --git a/fabcar/javascript/models/user.js b/fabcar/javascript/models/user.js new file mode 100644 index 0000000..467319a --- /dev/null +++ b/fabcar/javascript/models/user.js @@ -0,0 +1,77 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + + +const userSchema = new Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + password: { + type: String, + required: true + }, + publicKey: { + type: String, + required: true + }, + cart: { + myFiles: [ + { + myFileId: {type: Schema.Types.ObjectId, ref: 'File', required: true} + } + ] + }, + reqs: { + notifications: [ + { + requesterId: {type: Schema.Types.ObjectId, required: true}, + requestedFileId: {type: Schema.Types.ObjectId, required: true}, + decided: {type: Boolean, required: true}, + nonce: {type: String, required: false}, + } + ] + }, + dcart: { + allRequests: [ + { + isAccept: {type: Number, required: true}, + ownerId: {type: Schema.Types.ObjectId, required: false}, + requestedFileId: {type: Schema.Types.ObjectId, required: false}, + fileContent: {type: String, required: false}, + noncePlain: {type: String, required: false}, //plain random number as string + nonceGet: {type: String, required: false} + } + ] + } +}); + + +userSchema.methods.addToCart = function(fileId) { + const updatedFileItems = [...this.cart.myFiles]; + + updatedFileItems.push({ + myFileId: fileId + }); + const updatedCart = { + myFiles: updatedFileItems + }; + this.cart = updatedCart; + + return this.save(); +}; + +userSchema.methods.deleteFromCart = function(fileId){ + const updatedCartItems = this.cart.myFiles.filter(item => { + return item.myFileId != fileId; + }); + this.cart.myFiles = updatedCartItems; + return this.save(); +} + +module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/fabcar/javascript/package.json b/fabcar/javascript/package.json new file mode 100644 index 0000000..caa608d --- /dev/null +++ b/fabcar/javascript/package.json @@ -0,0 +1,60 @@ +{ + "name": "fabcar", + "version": "1.0.0", + "description": "FabCar application implemented in JavaScript", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4", + "bcryptjs": "^2.4.3", + "body-parser": "^1.19.0", + "connect-flash": "^0.1.1", + "connect-mongodb-session": "^2.4.1", + "csurf": "^1.11.0", + "ejs": "^3.1.6", + "express": "^4.17.1", + "express-session": "^1.17.1", + "express-validator": "^6.10.1", + "fs": "*", + "mongodb": "^3.6.6", + "mongoose": "^5.12.7", + "multer": "^1.4.2", + "mysql2": "^2.2.5", + "nodemon": "^2.0.7" + }, + "devDependencies": { + "chai": "^4.2.0", + "eslint": "^5.9.0", + "mocha": "^5.2.0", + "nyc": "^14.1.1", + "sinon": "^7.1.1", + "sinon-chai": "^3.3.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/fabcar/javascript/public/css/forms.css b/fabcar/javascript/public/css/forms.css new file mode 100644 index 0000000..20343be --- /dev/null +++ b/fabcar/javascript/public/css/forms.css @@ -0,0 +1,32 @@ +.form-control { + margin: 1rem 0; + align-self: center; +} + +.form-control label, +.form-control input, +.form-control textarea { + display: block; + width: 100%; + margin-bottom: 0.25rem; + align-items: center; +} + +.form-control input, +.form-control textarea { + border: 1px solid #a1a1a1; + font: inherit; + border-radius: 2px; +} + +.form-control input:focus, +.form-control textarea:focus { + outline-color: #00695c; +} + +.login-form { + width: 20rem; + max-width: 90%; + margin: auto; + display: block; +} diff --git a/fabcar/javascript/public/css/main.css b/fabcar/javascript/public/css/main.css new file mode 100644 index 0000000..3ce2f2b --- /dev/null +++ b/fabcar/javascript/public/css/main.css @@ -0,0 +1,271 @@ +@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700'); + +* { + box-sizing: border-box; +} + +body { + padding: 0; + margin: 0; + font-family: 'Open Sans', sans-serif; +} + +main { + padding: 1rem; + margin: auto; +} + +form { + display: inline; +} + +.centered { + text-align: center; +} + +.image { + height: 20rem; +} + +.image img { + height: 100%; +} + +.main-header { + width: 100%; + height: 3.5rem; + background-color: #00695c; + padding: 0 1.5rem; + display: flex; + align-items: center; +} + +.main-header__nav { + height: 100%; + width: 100%; + display: none; + align-items: center; + justify-content: space-between; +} + +.main-header__item-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; +} + +.main-header__item { + margin: 0 1rem; + padding: 0; +} + +.main-header__item a, +.main-header__item button { + font: inherit; + background: transparent; + border: none; + text-decoration: none; + color: white; + cursor: pointer; +} + +.main-header__item a:hover, +.main-header__item a:active, +.main-header__item a.active, +.main-header__item button:hover, +.main-header__item button:active { + color: #ffeb3b; +} + +.mobile-nav { + width: 30rem; + height: 100vh; + max-width: 90%; + position: fixed; + left: 0; + top: 0; + background: white; + z-index: 10; + padding: 2rem 1rem 1rem 2rem; + transform: translateX(-100%); + transition: transform 0.3s ease-out; +} + +.mobile-nav.open { + transform: translateX(0); +} + +.mobile-nav__item-list { + list-style: none; + display: flex; + flex-direction: column; + margin: 0; + padding: 0; +} + +.mobile-nav__item { + margin: 1rem; + padding: 0; +} + +.mobile-nav__item a, +.mobile-nav__item button { + font: inherit; + text-decoration: none; + color: black; + font-size: 1.5rem; + padding: 0.5rem 2rem; + background: transparent; + border: none; + cursor: pointer; +} + +.mobile-nav__item a:active, +.mobile-nav__item a:hover, +.mobile-nav__item a.active, +.mobile-nav__item button:hover, +.mobile-nav__item button:active { + background: #00695c; + color: white; + border-radius: 3px; +} + +#side-menu-toggle { + border: 1px solid white; + font: inherit; + padding: 0.5rem; + display: block; + background: transparent; + color: white; + cursor: pointer; +} + +#side-menu-toggle:focus { + outline: none; +} + +#side-menu-toggle:active, +#side-menu-toggle:hover { + color: #ffeb3b; + border-color: #ffeb3b; +} + +.backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: 5; + display: none; +} + +.grid { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + align-items: stretch; +} + +.card { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26); +} + +.card__header, +.card__content { + padding: 1rem; +} + +.card__header h1, +.card__content h1, +.card__content h2, +.card__content p { + margin: 0; +} + +.card__image { + width: 100%; +} + +.card__image img { + width: 100%; +} + +.card__actions { + padding: 1rem; + text-align: center; +} + +.card__actions button, +.card__actions a { + margin: 0 0.25rem; +} + +.btn { + display: inline-block; + padding: 0.25rem 1rem; + text-decoration: none; + font: inherit; + border: 1px solid #00695c; + color: #00695c; + background: white; + border-radius: 3px; + cursor: pointer; +} + +#logout{ + top: 30%; + position: relative; + align-items: center; + align-self: center; + color: #ffeb3b; +} + +.btn:hover, +.btn:active { + background-color: #00695c; + color: white; +} + +.btn.danger { + color: red; + border-color: red; +} + +.btn.danger:hover, +.btn.danger:active { + background: red; + color: white; +} + +.user-message { + margin: auto; + width: 90%; + border: 1px solid #4771fa; + padding: 0.5rem; + border-radius: 3px; + background: #b9c9ff; + text-align: center; +} + +.user-message--error { + border-color: red; + background: rgb(255, 176, 176); + color: red; +} + +@media (min-width: 768px) { + .main-header__nav { + display: flex; + } + + #side-menu-toggle { + display: none; + } + + .user-message { + width: 30rem; + } +} \ No newline at end of file diff --git a/fabcar/javascript/public/css/user.css b/fabcar/javascript/public/css/user.css new file mode 100644 index 0000000..7b4afad --- /dev/null +++ b/fabcar/javascript/public/css/user.css @@ -0,0 +1,30 @@ +.user-form { + width: 20rem; + max-width: 90%; + margin: auto; + display: block; +} + +.user-item { + width: 20rem; + max-width: 95%; + margin: 1rem; + background-color: darkslategray; + color: chocolate; +} + +.user__name { + font-size: 1.2rem; + text-align: center; + color: green +} + +.user__email { + text-align: center; + color: #d36b6b; + margin-bottom: 0.5rem; +} + +.user__description { + text-align: center; +} \ No newline at end of file diff --git a/fabcar/javascript/public/files/2021-05-30T09:10:32.248Z-*-Thank Bob.txt b/fabcar/javascript/public/files/2021-05-30T09:10:32.248Z-*-Thank Bob.txt new file mode 100644 index 0000000000000000000000000000000000000000..7e4998fd0f3f3b880bbf38afb77a13c740d81e71 GIT binary patch literal 256 zcmV+b0ssCZ*5F{N3DUERff}BAe(Q3hJM=8K4v)J>h_FwmE^gYwv#rgIH4)UGvqi14 zxgvZE)GM%+o0+$x=fRrt=2+7Kr>~EmhPTTv#({!{eacqX<-z1&=?YIV$0d$FB>o7{ zT)_&Nv{gyUDeOME=HE*)H4+V<;;$#+LdglG49<497D^~+Z5RXm;e3^nED$d&X%(Fq z2z|*pbyC;TQgvKdn7@ph(6{8$vO;85@Mk0kOZR_cJ2f?$j{tHX>SI$n1F=6q!Z**R z1_AMJJJbGgsm8*eq754>Gb*=);Qbn3f@h+(@kT!&`!a!+3I*DP>i!0yurnci>hiep G;FMdeF@Pxm literal 0 HcmV?d00001 diff --git a/fabcar/javascript/public/files/2021-05-30T15:26:42.220Z-*-Angry Bob.txt b/fabcar/javascript/public/files/2021-05-30T15:26:42.220Z-*-Angry Bob.txt new file mode 100644 index 0000000000000000000000000000000000000000..c421b45c782dc7e0ad48a4098bc3a84327d5402d GIT binary patch literal 256 zcmV+b0ssD&cfLCksYPH$hJ>hXa3a#X-qu@(#`X_Pp+DRGX*TLZXU^QSPNt5bxPaUe zp-1};npQFI^c_kyX8gBX1ja#u~C5u`MER6HT4l-L&(kebz}ORtnK)Qu<*)ZKW2`%NNgY6u+F&-O*L_eB0$^T7hEH5ZcXf GFxP5rfQRV- literal 0 HcmV?d00001 diff --git a/fabcar/javascript/public/files/2021-05-30T20:15:31.073Z-*-BobNew1.txt b/fabcar/javascript/public/files/2021-05-30T20:15:31.073Z-*-BobNew1.txt new file mode 100644 index 0000000000000000000000000000000000000000..951eb476c557ee2512eac38ddab7f4c7187a4c57 GIT binary patch literal 256 zcmV+b0ssCAKKts#Yfu9nw}69FXTNkqg|LNPkM&R``SPkM2YwTE($p1YK(H@{Kj`4j zsD7fyH^tnXgu!un+u~w)mR5kV%FRe!a5L}9=M*)<0{jo$05(~gsxvn~#J zYW5(f?uaA*5QDl`k1#{bc4R#rXmCrPObgwr@V%U0=l*j2qQ-tPs?p!{4C=w7jo-ME zB9Aem+M}8-Xc%%=CL4bqAj|D$l0C}#KSve?SkPFQK4vLXgf+u`FZ%wYA z8PFQq(76y@q<$94i-%`ZMrb$@yFczz0RXd Guo`@hn1IIs literal 0 HcmV?d00001 diff --git a/fabcar/javascript/public/files/2021-05-31T05:18:32.110Z-*-Management-Repy.txt b/fabcar/javascript/public/files/2021-05-31T05:18:32.110Z-*-Management-Repy.txt new file mode 100644 index 0000000000000000000000000000000000000000..7ec135abc4bea83ed5d5d9c10175c765ba5c12aa GIT binary patch literal 256 zcmV+b0ssDmF@it=By%l`oEi#}OKRsOGMYHPIKqBgCb?@xP1Nouf|z3m zbOT%2Zuh6}>zQoj)_*Lx?^EJxKe!PzSQ1oVjH)&f1A`OULRsJ{SRfPPvZM`e^PepC zt^10&_&rguSKYX5RpmUHGlJJ6k`XDL_U93SNfq#%u?%Bb9;MDXjYa=M;oCI>F{Y)ImV;QN!gY&>J)kh zS#}`rLgkaMx;yihp|Ws;vc4T9nOuF{!1-hNK~rAnI9PNrup2fx1u+z3Y@Zt9Tth7o G`K0P;?RS^} literal 0 HcmV?d00001 diff --git a/fabcar/javascript/public/files/2021-05-31T14:18:08.451Z-*-test.txt b/fabcar/javascript/public/files/2021-05-31T14:18:08.451Z-*-test.txt new file mode 100644 index 0000000000000000000000000000000000000000..ecc2111ba585a4fd6b28f6bf1b23b91ff0a6a5e5 GIT binary patch literal 256 zcmV+b0ssCQMIiGGk`#qk%rPT>JLndJ>9)ko0M;sW%J~5O3XE__PN0}>h+`P4L8{D* z(K%qjwFnUIPsKscv|&BQFH>C2tDCfr_40t)7*E~Mss6laN`vy-bZX$OtG^A%!r4UD z*9rA<7uA=`u@^UeF{$YD&7T}Ax)4RUsc(-%L0dkyCqfq@b^h8g&RQsqMF1y|_Q5l+ z8M%o;#vj8B)_T5=Jb)Wb+mbmLzM!h8rsuLFhA=~zP&^f{cboV_Sxe}P;jK24Z1VG$ zPJA4cNZ8E|z9<~WH=9_k+qv}v1whS(DlJBK5HEh0`lS`r{*2Z<_ZU!8-Iyu_i7Qgie=&ieNn0qcciWA5vLujrWIO!WX84WCqUQU;H;;^%Pc|R(aXevm6R57qT ze>i1f6Uv+Fe`+Kn(#-Xk6jGxrR2t-?{Cjy{8Zv>X6c>n|SUVB<<9=bcdNUflUzp~S ze|>9`T}STtz&=#ttiX`IG}x{8P^e-L?bV->myR|5zA45rN*OnIPL9dY2>A03d7|-U zRntQJjY5hsc_~|aWIpFjUR*HS??sz$nn$0pCIdH(n%}Mhs9dJ-wp~UUZI literal 0 HcmV?d00001 diff --git a/fabcar/javascript/public/files/2021-05-31T14:46:10.484Z-*-BaghaFile1.txt b/fabcar/javascript/public/files/2021-05-31T14:46:10.484Z-*-BaghaFile1.txt new file mode 100644 index 0000000000000000000000000000000000000000..b45d4e4e9683834db3f2c509900a7a4d150bd88e GIT binary patch literal 256 zcmV+b0ssDh=EvF<2**L3jK@>e=|bf!AjU#!J+9zC3>Frw6h{Ha8#BFS?#xr&iVtoR z*kr5EZ=2O!I%sV#eAO7z2F6aktqfxbufnQgG3voAr-aV%DC-B=25+C&NCM~0eX>+!Wz$4TCyZurm>k_VJCMWg)LEJ#o5E9eTz< z-}oJPEfPTtLg}J4?s)4Y1Kq;D%OgpXoqz_^sYxL3wW-$`-x!o{VvR31U¿Æí‰dï÷(©ã!PP£ ‚KÞ'µw’<4`çžVð"/Ù\oê5á§ñ’ûà/Ÿ*\ÈR’½¾~ ÿ¦o«q’êí¢è±_DMgí¿*¢cýYámû‹C0b¥Œ5;M«ål;wŸÔÐA„¹$å@f?yä1Ñ=‰éßh4>ãßö]‰j§}fØ= ¢Žzëwì¦[s¬?Åú#<ïךTïzôñúŨ:G ›çÏ,¶½ÑTÿaHú·|´ï-†‘/H{¸¤¬.Oƒ„*ó.ñt êPp£[}xv›‡Ù··ß½i«þ)ÚÃ]c¾ÞTÑO×Çkø·î \ No newline at end of file diff --git a/fabcar/javascript/public/files/2021-07-04T11:04:16.743Z-*-random.txt b/fabcar/javascript/public/files/2021-07-04T11:04:16.743Z-*-random.txt new file mode 100644 index 0000000..4610381 --- /dev/null +++ b/fabcar/javascript/public/files/2021-07-04T11:04:16.743Z-*-random.txt @@ -0,0 +1,2 @@ + ¾“ñÐou¿¹‘á-¹L¥äy pøØX^Ȳ-hUÁs3h¶‰y{ðηȾĢG9z•ñK’Wˆùé®)_Mû*Âʨܡõ†ßÔè·Æ.f{ÊfgLê´#æF9 „©@H£ .mqi¬VÎËmDÑ·§D:WÔ”SpVõÝiü*iÍΛ»üU*¦ºÏ +iCn8/žeMû·C–0pdÊ€|Öí¹?C?p \ No newline at end of file diff --git a/fabcar/javascript/query.js b/fabcar/javascript/query.js new file mode 100644 index 0000000..82f5367 --- /dev/null +++ b/fabcar/javascript/query.js @@ -0,0 +1,58 @@ +/* + * 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 fs = require('fs'); + + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const identity = await wallet.get('appUser'); + if (!identity) { + console.log('An identity for the user "appUser" does not exist in the wallet'); + console.log('Run the registerUser.js application before retrying'); + return; + } + + // Create a new gateway for connecting to our peer node. + const gateway = new Gateway(); + await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } }); + + // Get the network (channel) our contract is deployed to. + const network = await gateway.getNetwork('mychannel'); + + // Get the contract from the network. + const contract = network.getContract('fabcar'); + + // Evaluate the specified transaction. + // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4') + // queryAllCars transaction - requires no arguments, ex: ('queryAllCars') + const result = await contract.evaluateTransaction('queryAllCars'); + console.log(`Transaction has been evaluated, result is: ${result.toString()}`); + + // Disconnect from the gateway. + await gateway.disconnect(); + + } catch (error) { + console.error(`Failed to evaluate transaction: ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/javascript/registerUser.js b/fabcar/javascript/registerUser.js new file mode 100644 index 0000000..f8fdd04 --- /dev/null +++ b/fabcar/javascript/registerUser.js @@ -0,0 +1,75 @@ +/* + * 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 fs = require('fs'); +const path = require('path'); + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new CA client for interacting with the CA. + const caURL = ccp.certificateAuthorities['ca.org1.example.com'].url; + const ca = new FabricCAServices(caURL); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const userIdentity = await wallet.get('appUser'); + if (userIdentity) { + console.log('An identity for the user "appUser" already exists in the wallet'); + return; + } + + // Check to see if we've already enrolled the admin user. + const adminIdentity = await wallet.get('admin'); + if (!adminIdentity) { + console.log('An identity for the admin user "admin" does not exist in the wallet'); + console.log('Run the enrollAdmin.js application 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, 'admin'); + + // Register the user, enroll the user, and import the new identity into the wallet. + const secret = await ca.register({ + affiliation: 'org1.department1', + enrollmentID: 'appUser', + role: 'client' + }, adminUser); + const enrollment = await ca.enroll({ + enrollmentID: 'appUser', + enrollmentSecret: secret + }); + const x509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org1MSP', + type: 'X.509', + }; + await wallet.put('appUser', x509Identity); + console.log('Successfully registered and enrolled admin user "appUser" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to register user "appUser": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/javascript/rosalind.txt b/fabcar/javascript/rosalind.txt new file mode 100644 index 0000000000000000000000000000000000000000..b1245132c741539c7dda0d237f1f70b99bae49f5 GIT binary patch literal 256 zcmV+b0ssDtwOTxq_-@mBf22gh3KF=S3ZahNY-61qHdW&oMs?%028g5FywP%xsnr!7 z>zWw!D=mM96eJ?7#`YnJ { + return User.findOne({ email: value }).then(stuDoc => { + if (!stuDoc) { + return Promise.reject( + 'No user with this email. Do you want to signUp ?' + ); + } + }); + }) + .normalizeEmail(), + body('password', 'Password has to be valid.') + .isLength({ min: 4 }) + ], +indexController.postLogin); + + + + + +router.get('/signup', indexController.getSignup); +router.post('/signup', +[ + check('email') + .isEmail() + .withMessage('Please enter a valid email.') + .custom((value, { req }) => { + return User.findOne({ email: value }).then(stuDoc => { + if (stuDoc) { + return Promise.reject( + 'E-Mail exists already, please pick a different one.' + ); + } + }); + }) + .normalizeEmail(), + body('name').isString(), + body( + 'password', + 'Please enter a password with at least 4 characters.' + ) + .isLength({ min: 4 }), + body('confirmPassword') + .custom((value, { req }) => { + if (value !== req.body.password) { + throw new Error('Passwords have to match!'); + } + return true; + }) + ], + indexController.postSignup); + + +router.post("/logout", indexController.postLogout); + + +module.exports = router; \ No newline at end of file diff --git a/fabcar/javascript/routes/user.js b/fabcar/javascript/routes/user.js new file mode 100644 index 0000000..f914453 --- /dev/null +++ b/fabcar/javascript/routes/user.js @@ -0,0 +1,45 @@ +var express = require('express'); +const userController1 = require('../controllers/userUpDownDel'); +const userController2 = require('../controllers/userShowReq'); +const isAuth = require('../middleware/is-auth'); +var router = express.Router(); + +// Upload - Download related. +router.get('/', userController1.getFrontPage); + +router.get('/upload', userController1.getUploadFile); +router.post('/upload', userController1.postUploadFile); + +router.get('/download', userController1.getDownloadFile); +router.post('/download', userController1.postDownloadFile); + + +router.get('/uploaded', isAuth, userController1.getUploadedFiles); +router.get('/downloaded', userController1.getDownloadedFiles); + +router.post('/delete-file/:myFileId', userController1.deleteFile); + + + + +// Show and request-handling related. + + +router.get('/show-file/:myFileId', userController2.showFileById); + +router.get('/notification', userController2.getAllNotifications); + + +router.get('/show-decrypted-content/:fileContent/:noncePlain/:nonceGet', userController2.showDecryptedFileContent); + +router.get('/request', userController2.getAllRequests); + + + +router.post('/request-file/:ownerId/:fileName', userController2.requestFile); + +router.post('/grant-permission/:requesterId/:requestedFileId', userController2.grantPermission); + +router.post('/deny-permission/:requesterId/:requestedFileId', userController2.denyPermission); + +module.exports = router; \ No newline at end of file diff --git a/fabcar/javascript/temp-file/againEncryptFile.txt b/fabcar/javascript/temp-file/againEncryptFile.txt new file mode 100644 index 0000000..abf7573 --- /dev/null +++ b/fabcar/javascript/temp-file/againEncryptFile.txt @@ -0,0 +1 @@ +19a6c9db8f17fad0864923ad3a83fef7532c0c2652d95d5283fa450aed3257732bc43453c496b6587e3dcf8aecbfa6551275ab27dccb5ad7dc9e7f42e606e9c42e6e2ae32e06665311b69b792a3bcdcbcf13cc0861dfefa3b45d81730ebf2ad8e3bdbd233562f6e6e1bc3e85c6262c48f619a6f08175679be9a7d5b96160e58b83bd44e249c301b6a5aaff5662a314a95c065b392441d3a6756f32ba44a1056316858c42d70755bb6e1b5dd8fc079a4c04e45da6c82a042e14b88c69da34425df80ab668c23e8c9216cf74897fc17b026ba2f3a52571d59d5d68cbe6e87a90fe496d4197d070acd093b2e0fc31256ce0053034de449b08046a41b2d996a52ead \ No newline at end of file diff --git a/fabcar/javascript/temp-file/decryptedFile.txt b/fabcar/javascript/temp-file/decryptedFile.txt new file mode 100644 index 0000000..58200e3 --- /dev/null +++ b/fabcar/javascript/temp-file/decryptedFile.txt @@ -0,0 +1 @@ +Now Bob sends a msg to Alice!!!! diff --git a/fabcar/javascript/util.js b/fabcar/javascript/util.js new file mode 100644 index 0000000..7bddce2 --- /dev/null +++ b/fabcar/javascript/util.js @@ -0,0 +1,57 @@ +const path = require('path'); +const fs = require('fs'); + +// FABRIC INCLUDES STARTS +const { Gateway, Wallets } = require('fabric-network'); +const ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json'); +const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); +// FABRIC INCLUDES ENDS + +var init = (async () => { + // Create a new file system based wallet for managing identities. + // const walletPath = path.join(process.cwd(), "wallet"); + // const wallet = new FileSystemWallet(walletPath); + // console.log(`Wallet path: ${walletPath}`); + const walletPath = path.join(__dirname, 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + // const userExists = await wallet.exists("user1"); + // if (!userExists) { + // console.log( + // 'An identity for the user "user1" does not exist in the wallet' + // ); + // console.log("Run the registerUser.js application before retrying"); + // return; + // } + const identity = await wallet.get('appUser'); + if (!identity) { + console.log('An identity for the user "appUser" does not exist in the wallet'); + console.log('Run the registerUser.js application before retrying'); + return; + } + + // Create a new gateway for connecting to our peer node. + const gateway = new Gateway(); + await gateway.connect(ccp, { wallet, identity: 'appUser' }); + // , discovery: { enabled: true, asLocalhost: true } // After indentity: 'appUser' + + + // Get the network (channel) our contract is deployed to. + const network = await gateway.getNetwork("mychannel"); + + // Get the contract from the network. + const contract = network.getContract("fabcar"); + + return { + getInput: function () { + return { + contact: contract, // Will be either inc or exp + gateway: gateway, + }; + }, + }; + })(); + + module.exports = init; \ No newline at end of file diff --git a/fabcar/javascript/views/404.ejs b/fabcar/javascript/views/404.ejs new file mode 100644 index 0000000..3aaa94b --- /dev/null +++ b/fabcar/javascript/views/404.ejs @@ -0,0 +1,10 @@ +<%- include('includes/head.ejs') %> + + + + <%- include('includes/navigation.ejs') %> +

Page Not Found!

+

No such route.

+ + +<%- include('includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/auth/login.ejs b/fabcar/javascript/views/auth/login.ejs new file mode 100644 index 0000000..2b6c351 --- /dev/null +++ b/fabcar/javascript/views/auth/login.ejs @@ -0,0 +1,32 @@ +<%- include('../includes/head.ejs') %> + + + + + + <%- include('../includes/navigation.ejs') %> + +
+ <% if (errorMessage) { %> +
<%= errorMessage %>
+ <% } %> + +
+<%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/auth/signup.ejs b/fabcar/javascript/views/auth/signup.ejs new file mode 100644 index 0000000..7ef360f --- /dev/null +++ b/fabcar/javascript/views/auth/signup.ejs @@ -0,0 +1,45 @@ +<%- include('../includes/head.ejs') %> + + + + + + <%- include('../includes/navigation.ejs') %> + +
+ <% if (errorMessage) { %> +
<%= errorMessage %>
+ <% } %> + +
+<%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/form.ejs b/fabcar/javascript/views/form.ejs new file mode 100644 index 0000000..9e55bf7 --- /dev/null +++ b/fabcar/javascript/views/form.ejs @@ -0,0 +1,36 @@ + + + + + + Register + + + +
+

Upload File Form

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ + \ No newline at end of file diff --git a/fabcar/javascript/views/includes/end.ejs b/fabcar/javascript/views/includes/end.ejs new file mode 100644 index 0000000..7cbf3b0 --- /dev/null +++ b/fabcar/javascript/views/includes/end.ejs @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/fabcar/javascript/views/includes/head.ejs b/fabcar/javascript/views/includes/head.ejs new file mode 100644 index 0000000..ad79723 --- /dev/null +++ b/fabcar/javascript/views/includes/head.ejs @@ -0,0 +1,9 @@ + + + + + + + + <%= pageTitle %> + \ No newline at end of file diff --git a/fabcar/javascript/views/includes/navigation.ejs b/fabcar/javascript/views/includes/navigation.ejs new file mode 100644 index 0000000..2d5419b --- /dev/null +++ b/fabcar/javascript/views/includes/navigation.ejs @@ -0,0 +1,86 @@ +
+
+ + +
+ + \ No newline at end of file diff --git a/fabcar/javascript/views/index.ejs b/fabcar/javascript/views/index.ejs new file mode 100644 index 0000000..ab4b18b --- /dev/null +++ b/fabcar/javascript/views/index.ejs @@ -0,0 +1,16 @@ + + + + + + SSE Implementation + + +
+

Home

+ Form + + +
+ + \ No newline at end of file diff --git a/fabcar/javascript/views/updown/download.ejs b/fabcar/javascript/views/updown/download.ejs new file mode 100644 index 0000000..98cd6de --- /dev/null +++ b/fabcar/javascript/views/updown/download.ejs @@ -0,0 +1,20 @@ +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+ + +
+ <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/downloaded.ejs b/fabcar/javascript/views/updown/downloaded.ejs new file mode 100644 index 0000000..0887e5c --- /dev/null +++ b/fabcar/javascript/views/updown/downloaded.ejs @@ -0,0 +1,14 @@ +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+

+ List of Downloaded files here.. +

+ +
+ <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/notification.ejs b/fabcar/javascript/views/updown/notification.ejs new file mode 100644 index 0000000..7fc92a0 --- /dev/null +++ b/fabcar/javascript/views/updown/notification.ejs @@ -0,0 +1,55 @@ + +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+ <% if (notifications.length > 0) { %> +
+ <% for (let notification of notifications) { %> +
+
+

+ Requester Id : <%= notification.requesterId %> +

+ +
+ + <% if(notification.decided == false) { %> + +
+
+ + +
+ +
+ +
+
+ + +
+
+ + <% } else { %> + +
+

Already decided.

+
+ <% } %> + +
+ <% } %> +
+ <% } else { %> +

Still No Notifications!

+ <% } %> +
+ + <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/request.ejs b/fabcar/javascript/views/updown/request.ejs new file mode 100644 index 0000000..58093fa --- /dev/null +++ b/fabcar/javascript/views/updown/request.ejs @@ -0,0 +1,45 @@ + +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+ <% if (requests.length > 0) { %> +
+ <% for (let request of requests) { %> + <% if (request.isAccept != 0) { %> + <% if (request.isAccept == 2) { %> +
+
+

+ <%= request.ownerId %> Accepted your request to access. +

+
+ +
+ Show Content + +
+ +
+ <% } else { %> +
+
+

+ <%= request.ownerId %> denied your request to access. +

+
+
+ <% } %> + <% } %> + <% } %> +
+ <% } else { %> +

Still No Requests!

+ <% } %> +
+ + <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/searchResult.ejs b/fabcar/javascript/views/updown/searchResult.ejs new file mode 100644 index 0000000..f3c8199 --- /dev/null +++ b/fabcar/javascript/views/updown/searchResult.ejs @@ -0,0 +1,52 @@ +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+ <% if (errorMessage) { %> +
<%= errorMessage %>
+ <% } %> + + + <% if (docs.length > 0) { %> +
+ <% for (let doc of docs) { %> +
+
+

+ Doc name = <%= doc[0] %> +

+

+ Matched with entered keywords = <%= doc[1] %> times. +

+ + +
+ +
+ +
+ + +
+ +
+ +
+ <% } %> +
+ <% } else { %> +

No docs Found!

+ <% } %> + + + + + +
+ <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/showDecryptedContent.ejs b/fabcar/javascript/views/updown/showDecryptedContent.ejs new file mode 100644 index 0000000..8938699 --- /dev/null +++ b/fabcar/javascript/views/updown/showDecryptedContent.ejs @@ -0,0 +1,17 @@ +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+ <% if (verification == true) { %> +

<%= documents %>

+ <% } else { %> +

ALART!!!

+

File is not received from the actual owner. File content can be compromised

+ <% } %> + +
+ <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/showFile.ejs b/fabcar/javascript/views/updown/showFile.ejs new file mode 100644 index 0000000..85fd0b2 --- /dev/null +++ b/fabcar/javascript/views/updown/showFile.ejs @@ -0,0 +1,12 @@ +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+

<%= fileContent %>

+ +
+ <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/upload.ejs b/fabcar/javascript/views/updown/upload.ejs new file mode 100644 index 0000000..a28966f --- /dev/null +++ b/fabcar/javascript/views/updown/upload.ejs @@ -0,0 +1,34 @@ +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+ + <% if (errorMessage) { %> +
<%= errorMessage %>
+ <% } %> + + + +
+ <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/updown/uploaded.ejs b/fabcar/javascript/views/updown/uploaded.ejs new file mode 100644 index 0000000..5ee1041 --- /dev/null +++ b/fabcar/javascript/views/updown/uploaded.ejs @@ -0,0 +1,38 @@ + +<%- include('../includes/head.ejs') %> + + + + + <%- include('../includes/navigation.ejs') %> + +
+ <% if (files.length > 0) { %> +
+ <% for (let file of files) { %> +
+
+

+ File Id : <%= file.myFileId %> +

+
+ + +
+ Show +
+ + +
+ +
+ +
+ <% } %> +
+ <% } else { %> +

No files Found!

+ <% } %> +
+ + <%- include('../includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/javascript/views/users.ejs b/fabcar/javascript/views/users.ejs new file mode 100644 index 0000000..d79a928 --- /dev/null +++ b/fabcar/javascript/views/users.ejs @@ -0,0 +1,14 @@ +<%- include('./includes/head.ejs') %> + + + + + <%- include('./includes/navigation.ejs') %> + +
+

+ Here I will show the user Details. +

+ +
+ <%- include('./includes/end.ejs') %> \ No newline at end of file diff --git a/fabcar/networkDown.sh b/fabcar/networkDown.sh new file mode 100755 index 0000000..c1bc231 --- /dev/null +++ b/fabcar/networkDown.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +# Exit on first error +set -ex + +# Bring the test network down +pushd ../test-network +./network.sh down +popd + +# clean out any old identites in the wallets +rm -rf javascript/wallet/* +rm -rf java/wallet/* +rm -rf typescript/wallet/* diff --git a/fabcar/startFabric.sh b/fabcar/startFabric.sh new file mode 100755 index 0000000..8ce18cb --- /dev/null +++ b/fabcar/startFabric.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +# Exit on first error +set -e + +# don't rewrite paths for Windows Git Bash users +export MSYS_NO_PATHCONV=1 +starttime=$(date +%s) +CC_SRC_LANGUAGE=${1:-"go"} +CC_SRC_LANGUAGE=`echo "$CC_SRC_LANGUAGE" | tr [:upper:] [:lower:]` + +if [ "$CC_SRC_LANGUAGE" = "go" -o "$CC_SRC_LANGUAGE" = "golang" ] ; then + CC_SRC_PATH="../chaincode/fabcar/go/" +elif [ "$CC_SRC_LANGUAGE" = "javascript" ]; then + CC_SRC_PATH="../chaincode/fabcar/javascript/" +elif [ "$CC_SRC_LANGUAGE" = "java" ]; then + CC_SRC_PATH="../chaincode/fabcar/java" +elif [ "$CC_SRC_LANGUAGE" = "typescript" ]; then + CC_SRC_PATH="../chaincode/fabcar/typescript/" +else + echo The chaincode language ${CC_SRC_LANGUAGE} is not supported by this script + echo Supported chaincode languages are: go, java, javascript, and typescript + exit 1 +fi + +# clean out any old identites in the wallets +rm -rf javascript/wallet/* +rm -rf java/wallet/* +rm -rf typescript/wallet/* +rm -rf go/wallet/* + +# launch network; create channel and join peer to channel +pushd ../test-network +./network.sh down +./network.sh up createChannel -ca -s couchdb +./network.sh deployCC -ccn fabcar -ccv 1 -cci initLedger -ccl ${CC_SRC_LANGUAGE} -ccp ${CC_SRC_PATH} +popd + +cat <=8", + "npm": ">=5" + }, + "scripts": { + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "pretest": "npm run lint", + "test": "nyc mocha -r ts-node/register src/**/*.spec.ts", + "build": "tsc", + "build:watch": "tsc -w", + "prepublishOnly": "npm run build" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4" + }, + "devDependencies": { + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.5", + "@types/node": "^10.12.10", + "@types/sinon": "^5.0.7", + "@types/sinon-chai": "^3.2.1", + "chai": "^4.2.0", + "mocha": "^5.2.0", + "nyc": "^14.1.1", + "sinon": "^7.1.1", + "sinon-chai": "^3.3.0", + "ts-node": "^7.0.1", + "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/fabcar/typescript/src/enrollAdmin.ts b/fabcar/typescript/src/enrollAdmin.ts new file mode 100644 index 0000000..e84139b --- /dev/null +++ b/fabcar/typescript/src/enrollAdmin.ts @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as FabricCAServices from 'fabric-ca-client'; +import { Wallets, X509Identity } from 'fabric-network'; +import * as fs from 'fs'; +import * as path from 'path'; + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', '..','test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new CA client for interacting with the CA. + const caInfo = ccp.certificateAuthorities['ca.org1.example.com']; + const caTLSCACerts = caInfo.tlsCACerts.pem; + const ca = new FabricCAServices(caInfo.url, { trustedRoots: caTLSCACerts, verify: false }, caInfo.caName); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the admin user. + const identity = await wallet.get('admin'); + if (identity) { + console.log('An identity for the admin user "admin" already exists in the wallet'); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + const enrollment = await ca.enroll({ enrollmentID: 'admin', enrollmentSecret: 'adminpw' }); + const x509Identity: X509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org1MSP', + type: 'X.509', + }; + await wallet.put('admin', x509Identity); + console.log('Successfully enrolled admin user "admin" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to enroll admin user "admin": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/typescript/src/invoke.ts b/fabcar/typescript/src/invoke.ts new file mode 100644 index 0000000..762263e --- /dev/null +++ b/fabcar/typescript/src/invoke.ts @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway, Wallets } from 'fabric-network'; +import * as path from 'path'; +import * as fs from 'fs'; + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', '..','test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const identity = await wallet.get('appUser'); + if (!identity) { + console.log('An identity for the user "appUser" does not exist in the wallet'); + console.log('Run the registerUser.ts application before retrying'); + return; + } + + // Create a new gateway for connecting to our peer node. + const gateway = new Gateway(); + await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } }); + + // Get the network (channel) our contract is deployed to. + const network = await gateway.getNetwork('mychannel'); + + // Get the contract from the network. + const contract = network.getContract('fabcar'); + + // Submit the specified transaction. + // createCar transaction - requires 5 argument, ex: ('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom') + // changeCarOwner transaction - requires 2 args , ex: ('changeCarOwner', 'CAR12', 'Dave') + await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom'); + console.log(`Transaction has been submitted`); + + // Disconnect from the gateway. + await gateway.disconnect(); + + } catch (error) { + console.error(`Failed to submit transaction: ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/typescript/src/query.ts b/fabcar/typescript/src/query.ts new file mode 100644 index 0000000..b86677e --- /dev/null +++ b/fabcar/typescript/src/query.ts @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway, Wallets } from 'fabric-network'; +import * as path from 'path'; +import * as fs from 'fs'; + + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', '..','test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const identity = await wallet.get('appUser'); + if (!identity) { + console.log('An identity for the user "appUser" does not exist in the wallet'); + console.log('Run the registerUser.ts application before retrying'); + return; + } + + // Create a new gateway for connecting to our peer node. + const gateway = new Gateway(); + await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } }); + + // Get the network (channel) our contract is deployed to. + const network = await gateway.getNetwork('mychannel'); + + // Get the contract from the network. + const contract = network.getContract('fabcar'); + + // Evaluate the specified transaction. + // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4') + // queryAllCars transaction - requires no arguments, ex: ('queryAllCars') + const result = await contract.evaluateTransaction('queryAllCars'); + console.log(`Transaction has been evaluated, result is: ${result.toString()}`); + + } catch (error) { + console.error(`Failed to evaluate transaction: ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/typescript/src/registerUser.ts b/fabcar/typescript/src/registerUser.ts new file mode 100644 index 0000000..45c07d3 --- /dev/null +++ b/fabcar/typescript/src/registerUser.ts @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Wallets, X509Identity } from 'fabric-network'; +import * as FabricCAServices from 'fabric-ca-client'; +import * as path from 'path'; +import * as fs from 'fs'; + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', '..', '..','test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); + let ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new CA client for interacting with the CA. + const caURL = ccp.certificateAuthorities['ca.org1.example.com'].url; + const ca = new FabricCAServices(caURL); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const userIdentity = await wallet.get('appUser'); + if (userIdentity) { + console.log('An identity for the user "appUser" already exists in the wallet'); + return; + } + + // Check to see if we've already enrolled the admin user. + const adminIdentity = await wallet.get('admin'); + if (!adminIdentity) { + console.log('An identity for the admin user "admin" does not exist in the wallet'); + console.log('Run the enrollAdmin.ts application 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, 'admin'); + + // Register the user, enroll the user, and import the new identity into the wallet. + const secret = await ca.register({ affiliation: 'org1.department1', enrollmentID: 'appUser', role: 'client' }, adminUser); + const enrollment = await ca.enroll({ enrollmentID: 'appUser', enrollmentSecret: secret }); + const x509Identity: X509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org1MSP', + type: 'X.509', + }; + await wallet.put('appUser', x509Identity); + console.log('Successfully registered and enrolled admin user "appUser" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to register user "appUser": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/fabcar/typescript/tsconfig.json b/fabcar/typescript/tsconfig.json new file mode 100644 index 0000000..8c96ea0 --- /dev/null +++ b/fabcar/typescript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "es2017", + "moduleResolution": "node", + "module": "commonjs", + "declaration": true, + "sourceMap": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/fabcar/typescript/tslint.json b/fabcar/typescript/tslint.json new file mode 100644 index 0000000..e08fd0b --- /dev/null +++ b/fabcar/typescript/tslint.json @@ -0,0 +1,22 @@ +{ + "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"], + "max-line-length": false + }, + "rulesDirectory": [] +} diff --git a/high-throughput/README.md b/high-throughput/README.md new file mode 100644 index 0000000..1314b56 --- /dev/null +++ b/high-throughput/README.md @@ -0,0 +1,197 @@ + + +# High-Throughput Network + +## Purpose +This network is used to understand how to properly design the chaincode data model when handling thousands of transactions per second which all +update the same asset in the ledger. A naive implementation would use a single key to represent the data for the asset, and the chaincode would +then attempt to update this key every time a transaction involving it comes in. However, when many transactions all come in at once, in the time +between when the transaction is simulated on the peer (i.e. read-set is created) and it's ready to be committed to the ledger, another transaction +may have already updated the same value. Thus, in the simple implementation, the read-set version will no longer match the version in the orderer, +and a large number of parallel transactions will fail. To solve this issue, the frequently updated value is instead stored as a series of deltas +which are aggregated when the value must be retrieved. In this way, no single row is frequently read and updated, but rather a collection of rows +is considered. + +## Use Case +The primary use case for this chaincode data model design is for applications in which a particular asset has an associated amount that is +frequently added to or removed from. For example, with a bank or credit card account, money is either paid to or paid out of it, and the amount +of money in the account is the result of all of these additions and subtractions aggregated together. A typical person's bank account may not be +used frequently enough to require highly-parallel throughput, but an organizational account used to store the money collected from customers on an +e-commerce platform may very well receive a very high number of transactions from all over the world all at once. In fact, this use case is the only +use case for crypto currencies like Bitcoin: a user's unspent transaction output (UTXO) is the result of all transactions he or she has been a part of +since joining the blockchain. Other use cases that can employ this technique might be IOT sensors which frequently update their sensed value in the +cloud. + +By adopting this method of storing data, an organization can optimize their chaincode to store and record transactions as quickly as possible and can +aggregate ledger records into one value at the time of their choosing without sacrificing transaction performance. Given the state-machine design of +Hyperledger Fabric, however, careful considerations need to be given to the data model design for the chaincode. + +Let's look at some concrete use cases and how an organization might implement high-throughput storage. These cases will try and explore some of the +advantages and disadvantages of such a system, and how to overcome them. + +#### Example 1 (IOT): Boxer Construction Analysts + +Boxer Construction Analysts is an IOT company focused on enabling real-time monitoring of large, expensive assets (machinery) on commercial +construction projects. They've partnered with the only construction vehicle company in New York, Condor Machines Inc., to provide a reliable, +auditable, and replayable monitoring system on their machines. This allows Condor to monitor their machines and address problems as soon as +they occur while providing end-users with a transparent report on machine health, which helps keep the customers satisfied. + +The vehicles are outfitted with many sensors each of which broadcasts updated values at frequencies ranging from several times a second to +several times a minute. Boxer initially sets up their chaincode so that the central machine computer pushes these values out to the blockchain +as soon as they're produced, and each sensor has its own row in the ledger which is updated when a new value comes in. While they find that +this works fine for the sensors which only update several times a minute, they run into some issues when updating the faster sensors. Often, +the blockchain skips several sensor readings before adding a new one, defeating the purpose of having a fast, always-on sensor. The issue they're +running into is that they're sending update transactions so fast that the version of the row is changed between the creation of a transaction's +read-set and committing that transaction to the ledger. The result is that while a transaction is in the process of being committed, all future +transactions are rejected until the commitment process is complete and a new, much later reading updates the ledger. + +To address this issue, they adopt a high-throughput design for the chaincode data model instead. Each sensor has a key which identifies it within the +ledger, and the difference between the previous reading and the current reading is published as a transaction. For example, if a sensor is monitoring +engine temperature, rather than sending the following list: 220F, 223F, 233F, 227F, the sensor would send: +220, +3, +10, -6 (the sensor is assumed +to start a 0 on initialization). This solves the throughput problem, as the machine can post delta transactions as fast as it wants and they will all +eventually be committed to the ledger in the order they were received. Additionally, these transactions can be processed as they appear in the ledger +by a dashboard to provide live monitoring data. The only difference the engineers have to pay attention to in this case is to make sure the sensors can +send deltas from the previous reading, rather than fixed readings. + +#### Example 2 (Balance Transfer): Robinson Credit Co. + +Robinson Credit Co. provides credit and financial services to large businesses. As such, their accounts are large, complex, and accessed by many +people at once at any time of the day. They want to switch to blockchain, but are having trouble keeping up with the number of deposits and +withdrawals happening at once on the same account. Additionally, they need to ensure users never withdraw more money than is available +on an account, and transactions that do get rejected. The first problem is easy to solve, the second is more nuanced and requires a variety of +strategies to accommodate high-throughput storage model design. + +To solve throughput, this new storage model is leveraged to allow every user performing transactions against the account to make that transaction in terms +of a delta. For example, global e-commerce company America Inc. must be able to accept thousands of transactions an hour in order to keep up with +their customer's demands. Rather than attempt to update a single row with the total amount of money in America Inc's account, Robinson Credit Co. +accepts each transaction as an additive delta to America Inc's account. At the end of the day, America Inc's accounting department can quickly +retrieve the total value in the account when the sums are aggregated. + +However, what happens when American Inc. now wants to pay its suppliers out of the same account, or a different account also on the blockchain? +Robinson Credit Co. would like to be assured that America Inc.'s accounting department can't simply overdraw their account, which is difficult to +do while at the same enabling transactions to happen quickly, as deltas are added to the ledger without any sort of bounds checking on the final +aggregate value. There are a variety of solutions which can be used in combination to address this. + +Solution 1 involves polling the aggregate value regularly. This happens separate from any delta transaction, and can be performed by a monitoring +service setup by Robinson themselves so that they can at least be guaranteed that if an overdraw does occur, they can detect it within a known +number of seconds and respond to it appropriately (e.g. by temporarily shutting off transactions on that account), all of which can be automated. +Furthermore, thanks to the decentralized nature of Fabric, this operation can be performed on a peer dedicated to this function that would not +slow down or impact the performance of peers processing customer transactions. + +Solution 2 involves breaking up the submission and verification steps of the balance transfer. Balance transfer submissions happen very quickly +and don't bother with checking overdrawing. However, a secondary process reviews each transaction sent to the chain and keeps a running total, +verifying that none of them overdraw the account, or at the very least that aggregated withdrawals vs deposits balance out at the end of the day. +Similar to Solution 1, this system would run separate from any transaction processing hardware and would not incur a performance hit on the +customer-facing chain. + +Solution 3 involves individually tailoring the smart contracts between Robinson and America Inc, leveraging the power of chaincode to customize +spending limits based on solvency proofs. Perhaps a limit is set on withdrawal transactions such that anything below \$1000 is automatically processed +and assumed to be correct and at minimal risk to either company simply due to America Inc. having proved solvency. However, withdrawals above \$1000 +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. + +### 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. + +Change back into the `high-throughput` directory in `fabic-samples`. Start the network and deploy the chaincode by issuing the following command: +``` +./startFabric.sh +``` + +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 : 81 secs ... +``` + +The `high-throughput` chaincode is now ready to receive invocations. + +### Invoke the chaincode + +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: `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: `go run app.go update myvar 100 +` + +#### 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: `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. + +The format for pruning is: `go run app.go prune name` where `name` is the name of the variable to prune. + +Example: `go run app.go prune myvar` + +#### Delete +The format for delete is: `go run app.go delete name` where `name` is the name of the variable to delete. + +Example: `go run app.go delete myvar` + +### Test the Network + +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. + +The second function, `manyUpdatesTraditional`, submits 1000 transactions that attempt to upddate the same key in the world state 1000 times. + +Run the following command to create and update `testvar1` a 1000 times: +``` +go run app.go manyUpdates testvar1 100 + +``` + +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 + +When you are finished using the `high-throughput` chaincode, you can bring down the network and remove any accompanying artifacts using the `networkDown.sh` script. + +``` +./networkDown.sh +``` diff --git a/high-throughput/application-go/.gitignore b/high-throughput/application-go/.gitignore new file mode 100755 index 0000000..e9fec12 --- /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 0000000..100d7c8 --- /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 0000000..1249266 --- /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 0000000..521b2e4 --- /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 0000000..9471652 --- /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 0000000..eb77cd5 --- /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 0000000..3447f57 --- /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 0000000..9c81886 --- /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-rc1 diff --git a/high-throughput/application-go/go.sum b/high-throughput/application-go/go.sum new file mode 100644 index 0000000..466c403 --- /dev/null +++ b/high-throughput/application-go/go.sum @@ -0,0 +1,226 @@ +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/hyperledger/fabric-sdk-go v1.0.0-rc1 h1:cfDo/5ovUZf2dCz08fznUxxVYEWAT4yKJcAh9b+K9Mk= +github.com/hyperledger/fabric-sdk-go v1.0.0-rc1/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/go.mod b/high-throughput/chaincode-go/go.mod new file mode 100644 index 0000000..1eceb00 --- /dev/null +++ b/high-throughput/chaincode-go/go.mod @@ -0,0 +1,12 @@ +module github.com/hyperledger/fabric-samples/high-throughput/chaincode + +go 1.12 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 + github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect + golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + golang.org/x/text v0.3.2 // indirect + google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect +) diff --git a/high-throughput/chaincode-go/go.sum b/high-throughput/chaincode-go/go.sum new file mode 100644 index 0000000..748afa1 --- /dev/null +++ b/high-throughput/chaincode-go/go.sum @@ -0,0 +1,86 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +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/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +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/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 h1:VEm3tPRTCzq3J/1XpVERh1PbOSnshUVwx2G5s3cLiTw= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85/go.mod h1:HZK6PKLWrvdD/t0oSLiyaRaUM6fZ7qjJuOlb0zrn0mo= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 h1:WzttYAPO5xkQ87ZrxzEhvDZknfarSNu1PZt3NPMTE3Y= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/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-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-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +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/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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/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= diff --git a/high-throughput/chaincode-go/high-throughput.go b/high-throughput/chaincode-go/high-throughput.go new file mode 100644 index 0000000..cee451a --- /dev/null +++ b/high-throughput/chaincode-go/high-throughput.go @@ -0,0 +1,396 @@ +/* + * Copyright IBM Corp All Rights Reserved + * + * SPDX-License-Identifier: Apache-2.0 + * + * Demonstrates how to handle data in an application with a high transaction volume where the transactions + * all attempt to change the same key-value pair in the ledger. Such an application will have trouble + * as multiple transactions may read a value at a certain version, which will then be invalid when the first + * transaction updates the value to a new version, thus rejecting all other transactions until they're + * re-executed. + * Rather than relying on serialization of the transactions, which is slow, this application initializes + * a value and then accepts deltas of that value which are added as rows to the ledger. The actual value + * is then an aggregate of the initial value combined with all of the deltas. Additionally, a pruning + * function is provided which aggregates and deletes the deltas to update the initial value. This should + * be done during a maintenance window or when there is a lowered transaction volume, to avoid the proliferation + * of millions of rows of data. + * + * @author Alexandre Pauwels for IBM + * @created 17 Aug 2017 + */ + +package main + +/* Imports + * 4 utility libraries for formatting, handling bytes, reading and writing JSON, and string manipulation + * 2 specific Hyperledger Fabric specific libraries for Smart Contracts + */ +import ( + "fmt" + "strconv" + + "github.com/hyperledger/fabric-chaincode-go/shim" + pb "github.com/hyperledger/fabric-protos-go/peer" +) + +//SmartContract is the data structure which represents this contract and on which various contract lifecycle functions are attached +type SmartContract struct { +} + +// Define Status codes for the response +const ( + OK = 200 + ERROR = 500 +) + +// Init is called when the smart contract is instantiated +func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) pb.Response { + return shim.Success(nil) +} + +// Invoke routes invocations to the appropriate function in chaincode +// Current supported invocations are: +// - update, adds a delta to an aggregate variable in the ledger, all variables are assumed to start at 0 +// - get, retrieves the aggregate value of a variable in the ledger +// - prune, deletes all rows associated with the variable and replaces them with a single row containing the aggregate value +// - delete, removes all rows associated with the variable +func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) pb.Response { + // Retrieve the requested Smart Contract function and arguments + function, args := APIstub.GetFunctionAndParameters() + + // Route to the appropriate handler function to interact with the ledger appropriately + if function == "update" { + return s.update(APIstub, args) + } else if function == "get" { + return s.get(APIstub, args) + } else if function == "prune" { + return s.prune(APIstub, args) + } else if function == "delete" { + return s.delete(APIstub, args) + } else if function == "putstandard" { + return s.putStandard(APIstub, args) + } else if function == "getstandard" { + return s.getStandard(APIstub, args) + } else if function == "delstandard" { + return s.delStandard(APIstub, args) + } + + return shim.Error("Invalid Smart Contract function name.") +} + +/** + * Updates the ledger to include a new delta for a particular variable. If this is the first time + * this variable is being added to the ledger, then its initial value is assumed to be 0. The arguments + * to give in the args array are as follows: + * - args[0] -> name of the variable + * - args[1] -> new delta (float) + * - args[2] -> operation (currently supported are addition "+" and subtraction "-") + * + * @param APIstub The chaincode shim + * @param args The arguments array for the update invocation + * + * @return A response structure indicating success or failure with a message + */ +func (s *SmartContract) update(APIstub shim.ChaincodeStubInterface, args []string) pb.Response { + // Check we have a valid number of args + if len(args) != 3 { + return shim.Error("Incorrect number of arguments, expecting 3") + } + + // Extract the args + name := args[0] + op := args[2] + _, err := strconv.ParseFloat(args[1], 64) + if err != nil { + return shim.Error("Provided value was not a number") + } + + // Make sure a valid operator is provided + if op != "+" && op != "-" { + return shim.Error(fmt.Sprintf("Operator %s is unrecognized", op)) + } + + // Retrieve info needed for the update procedure + txid := APIstub.GetTxID() + compositeIndexName := "varName~op~value~txID" + + // Create the composite key that will allow us to query for all deltas on a particular variable + compositeKey, compositeErr := APIstub.CreateCompositeKey(compositeIndexName, []string{name, op, args[1], txid}) + if compositeErr != nil { + return shim.Error(fmt.Sprintf("Could not create a composite key for %s: %s", name, compositeErr.Error())) + } + + // Save the composite key index + compositePutErr := APIstub.PutState(compositeKey, []byte{0x00}) + if compositePutErr != nil { + return shim.Error(fmt.Sprintf("Could not put operation for %s in the ledger: %s", name, compositePutErr.Error())) + } + + return shim.Success([]byte(fmt.Sprintf("Successfully added %s%s to %s", op, args[1], name))) +} + +/** + * Retrieves the aggregate value of a variable in the ledger. Gets all delta rows for the variable + * and computes the final value from all deltas. The args array for the invocation must contain the + * following argument: + * - args[0] -> The name of the variable to get the value of + * + * @param APIstub The chaincode shim + * @param args The arguments array for the get invocation + * + * @return A response structure indicating success or failure with a message + */ +func (s *SmartContract) get(APIstub shim.ChaincodeStubInterface, args []string) pb.Response { + // Check we have a valid number of args + if len(args) != 1 { + return shim.Error("Incorrect number of arguments, expecting 1") + } + + name := args[0] + // Get all deltas for the variable + deltaResultsIterator, deltaErr := APIstub.GetStateByPartialCompositeKey("varName~op~value~txID", []string{name}) + if deltaErr != nil { + return shim.Error(fmt.Sprintf("Could not retrieve value for %s: %s", name, deltaErr.Error())) + } + defer deltaResultsIterator.Close() + + // Check the variable existed + if !deltaResultsIterator.HasNext() { + return shim.Error(fmt.Sprintf("No variable by the name %s exists", name)) + } + + // Iterate through result set and compute final value + var finalVal float64 + var i int + for i = 0; deltaResultsIterator.HasNext(); i++ { + // Get the next row + responseRange, nextErr := deltaResultsIterator.Next() + if nextErr != nil { + return shim.Error(nextErr.Error()) + } + + // Split the composite key into its component parts + _, keyParts, splitKeyErr := APIstub.SplitCompositeKey(responseRange.Key) + if splitKeyErr != nil { + return shim.Error(splitKeyErr.Error()) + } + + // Retrieve the delta value and operation + operation := keyParts[1] + valueStr := keyParts[2] + + // Convert the value string and perform the operation + value, convErr := strconv.ParseFloat(valueStr, 64) + if convErr != nil { + return shim.Error(convErr.Error()) + } + + switch operation { + case "+": + finalVal += value + case "-": + finalVal -= value + default: + return shim.Error(fmt.Sprintf("Unrecognized operation %s", operation)) + } + } + + return shim.Success([]byte(strconv.FormatFloat(finalVal, 'f', -1, 64))) +} + +/** + * Prunes a variable by deleting all of its delta rows while computing the final value. Once all rows + * have been processed and deleted, a single new row is added which defines a delta containing the final + * computed value of the variable. The args array contains the following argument: + * - args[0] -> The name of the variable to prune + * + * @param APIstub The chaincode shim + * @param args The args array for the prune invocation + * + * @return A response structure indicating success or failure with a message + */ +func (s *SmartContract) prune(APIstub shim.ChaincodeStubInterface, args []string) pb.Response { + // Check we have a valid number of ars + if len(args) != 1 { + return shim.Error("Incorrect number of arguments, expecting 1") + } + + // Retrieve the name of the variable to prune + name := args[0] + + // Get all delta rows for the variable + deltaResultsIterator, deltaErr := APIstub.GetStateByPartialCompositeKey("varName~op~value~txID", []string{name}) + if deltaErr != nil { + return shim.Error(fmt.Sprintf("Could not retrieve value for %s: %s", name, deltaErr.Error())) + } + defer deltaResultsIterator.Close() + + // Check the variable existed + if !deltaResultsIterator.HasNext() { + return shim.Error(fmt.Sprintf("No variable by the name %s exists", name)) + } + + // Iterate through result set computing final value while iterating and deleting each key + var finalVal float64 + var i int + for i = 0; deltaResultsIterator.HasNext(); i++ { + // Get the next row + responseRange, nextErr := deltaResultsIterator.Next() + if nextErr != nil { + return shim.Error(nextErr.Error()) + } + + // Split the key into its composite parts + _, keyParts, splitKeyErr := APIstub.SplitCompositeKey(responseRange.Key) + if splitKeyErr != nil { + return shim.Error(splitKeyErr.Error()) + } + + // Retrieve the operation and value + operation := keyParts[1] + valueStr := keyParts[2] + + // Convert the value to a float + value, convErr := strconv.ParseFloat(valueStr, 64) + if convErr != nil { + return shim.Error(convErr.Error()) + } + + // Delete the row from the ledger + deltaRowDelErr := APIstub.DelState(responseRange.Key) + if deltaRowDelErr != nil { + return shim.Error(fmt.Sprintf("Could not delete delta row: %s", deltaRowDelErr.Error())) + } + + // Add the value of the deleted row to the final aggregate + switch operation { + case "+": + finalVal += value + case "-": + finalVal -= value + default: + return shim.Error(fmt.Sprintf("Unrecognized operation %s", operation)) + } + } + + // Update the ledger with the final value + updateResp := s.update(APIstub, []string{name, strconv.FormatFloat(finalVal, 'f', -1, 64), "+"}) + if updateResp.Status == ERROR { + return shim.Error(fmt.Sprintf("Could not update the final value of the variable after pruning: %s", updateResp.Message)) + } + + return shim.Success([]byte(fmt.Sprintf("Successfully pruned variable %s, final value is %f, %d rows pruned", args[0], finalVal, i))) +} + +/** + * Deletes all rows associated with an aggregate variable from the ledger. The args array + * contains the following argument: + * - args[0] -> The name of the variable to delete + * + * @param APIstub The chaincode shim + * @param args The arguments array for the delete invocation + * + * @return A response structure indicating success or failure with a message + */ +func (s *SmartContract) delete(APIstub shim.ChaincodeStubInterface, args []string) pb.Response { + // Check there are a correct number of arguments + if len(args) != 1 { + return shim.Error("Incorrect number of arguments, expecting 1") + } + + // Retrieve the variable name + name := args[0] + + // Delete all delta rows + deltaResultsIterator, deltaErr := APIstub.GetStateByPartialCompositeKey("varName~op~value~txID", []string{name}) + if deltaErr != nil { + return shim.Error(fmt.Sprintf("Could not retrieve delta rows for %s: %s", name, deltaErr.Error())) + } + defer deltaResultsIterator.Close() + + // Ensure the variable exists + if !deltaResultsIterator.HasNext() { + return shim.Error(fmt.Sprintf("No variable by the name %s exists", name)) + } + + // Iterate through result set and delete all indices + var i int + for i = 0; deltaResultsIterator.HasNext(); i++ { + responseRange, nextErr := deltaResultsIterator.Next() + if nextErr != nil { + return shim.Error(fmt.Sprintf("Could not retrieve next delta row: %s", nextErr.Error())) + } + + deltaRowDelErr := APIstub.DelState(responseRange.Key) + if deltaRowDelErr != nil { + return shim.Error(fmt.Sprintf("Could not delete delta row: %s", deltaRowDelErr.Error())) + } + } + + return shim.Success([]byte(fmt.Sprintf("Deleted %s, %d rows removed", name, i))) +} + +/** + * Converts a float64 to a byte array + * + * @param f The float64 to convert + * + * @return The byte array representation + */ +func f2barr(f float64) []byte { + str := strconv.FormatFloat(f, 'f', -1, 64) + + return []byte(str) +} + +// The main function is only relevant in unit test mode. Only included here for completeness. +func main() { + + // Create a new Smart Contract + err := shim.Start(new(SmartContract)) + if err != nil { + fmt.Printf("Error creating new Smart Contract: %s", err) + } +} + +/** + * All functions below this are for testing traditional editing of a single row + */ +func (s *SmartContract) putStandard(APIstub shim.ChaincodeStubInterface, args []string) pb.Response { + name := args[0] + valStr := args[1] + + _, getErr := APIstub.GetState(name) + if getErr != nil { + return shim.Error(fmt.Sprintf("Failed to retrieve the state of %s: %s", name, getErr.Error())) + } + + putErr := APIstub.PutState(name, []byte(valStr)) + if putErr != nil { + return shim.Error(fmt.Sprintf("Failed to put state: %s", putErr.Error())) + } + + return shim.Success(nil) +} + +func (s *SmartContract) getStandard(APIstub shim.ChaincodeStubInterface, args []string) pb.Response { + name := args[0] + + val, getErr := APIstub.GetState(name) + if getErr != nil { + return shim.Error(fmt.Sprintf("Failed to get state: %s", getErr.Error())) + } + + return shim.Success(val) +} + +func (s *SmartContract) delStandard(APIstub shim.ChaincodeStubInterface, args []string) pb.Response { + name := args[0] + + getErr := APIstub.DelState(name) + if getErr != nil { + return shim.Error(fmt.Sprintf("Failed to delete state: %s", getErr.Error())) + } + + return shim.Success(nil) +} diff --git a/high-throughput/networkDown.sh b/high-throughput/networkDown.sh new file mode 100755 index 0000000..6e898f5 --- /dev/null +++ b/high-throughput/networkDown.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +# Exit on first error +set -ex + +# 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/startFabric.sh b/high-throughput/startFabric.sh new file mode 100755 index 0000000..72ca5a7 --- /dev/null +++ b/high-throughput/startFabric.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +# Exit on first error +set -e + +# don't rewrite paths for Windows Git Bash users +export MSYS_NO_PATHCONV=1 +starttime=$(date +%s) +export TIMEOUT=10 +export DELAY=3 + +# launch network; create channel and join peer to channel +pushd ../test-network +./network.sh down + +echo "Bring up test network" +./network.sh up createChannel -ca +./network.sh deployCC -ccn bigdatacc -ccp ../high-throughput/chaincode-go/ -ccl go -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cci Init +popd +cat <B (PrincipalAmount * FixedRateBPS) / 100 + * B->A (PrincipalAmount * (ReferenceRateBPS + FloatingRateBPS)) / 100 + * We represent rates as basis points, with one basis point being equal to 1/100th + * of 1% (see https://www.investopedia.com/terms/b/basispoint.asp) + */ +type InterestRateSwap struct { + StartDate time.Time + EndDate time.Time + PaymentInterval time.Duration + PrincipalAmount uint64 + FixedRateBPS uint64 + FloatingRateBPS uint64 + ReferenceRate string +} + +/* +SwapManager is the chaincode that handles interest rate swaps. +The chaincode endorsement policy includes an auditing organization. +It provides the following functions: +-) createSwap: create swap with participants +-) calculatePayment: calculate what needs to be paid +-) settlePayment: mark payment done +-) setReferenceRate: for providers to set the reference rate + +The SwapManager stores three different kinds of information on the ledger: +-) the actual swap data ("swap" + ID) +-) the payment information ("payment" + ID), if "none", the payment has been settled +-) the reference rate ("rr" + ID) +*/ +type SwapManager struct { +} + +// Init callback +func (cc *SwapManager) Init(stub shim.ChaincodeStubInterface) pb.Response { + args := stub.GetArgs() + if len(args) < 5 { + return shim.Error("Insufficient number of arguments. Expected: ... ") + } + // set the limit above which the auditor needs to be involved, require it + // to be endorsed by the auditor + err := stub.PutState("audit_limit", args[2]) + if err != nil { + return shim.Error(err.Error()) + } + auditorEP, err := statebased.NewStateEP(nil) + if err != nil { + return shim.Error(err.Error()) + } + err = auditorEP.AddOrgs(statebased.RoleTypePeer, string(args[1])) + if err != nil { + return shim.Error(err.Error()) + } + epBytes, err := auditorEP.Policy() + if err != nil { + return shim.Error(err.Error()) + } + err = stub.SetStateValidationParameter("audit_limit", epBytes) + if err != nil { + return shim.Error(err.Error()) + } + + // create the reference rates, require them to be endorsed by the provider + for i := 3; i+1 < len(args); i += 2 { + org := string(args[i]) + rrID := "rr" + string(args[i+1]) + err = stub.PutState(rrID, []byte("0")) + if err != nil { + return shim.Error(err.Error()) + } + ep, err := statebased.NewStateEP(nil) + if err != nil { + return shim.Error(err.Error()) + } + err = ep.AddOrgs(statebased.RoleTypePeer, org) + if err != nil { + return shim.Error(err.Error()) + } + epBytes, err = ep.Policy() + if err != nil { + return shim.Error(err.Error()) + } + err = stub.SetStateValidationParameter(rrID, epBytes) + if err != nil { + return shim.Error(err.Error()) + } + } + + return shim.Success([]byte{}) +} + +// Invoke dispatcher +func (cc *SwapManager) Invoke(stub shim.ChaincodeStubInterface) pb.Response { + funcName, _ := stub.GetFunctionAndParameters() + if function, ok := functions[funcName]; ok { + fmt.Printf("Invoking %s\n", funcName) + return function(stub) + } + return shim.Error(fmt.Sprintf("Unknown function %s", funcName)) +} + +var functions = map[string]func(stub shim.ChaincodeStubInterface) pb.Response{ + "createSwap": createSwap, + "calculatePayment": calculatePayment, + "settlePayment": settlePayment, + "setReferenceRate": setReferenceRate, +} + +// Create a new swap among participants. +// The creation of the swap needs to be endorsed by the chaincode endorsement policy. +// Once created, the swap needs to be endorsed by its participants as well as the +// auditor in case the principal amount of the swap exceeds the audit threshold. +// This is enforced through the state-based endorsement policy that is set in this +// function. +// Parameters: swap ID, a JSONized InterestRateSwap, MSP ID of participant 1, +// MSP ID of participant 2 +func createSwap(stub shim.ChaincodeStubInterface) pb.Response { + _, parameters := stub.GetFunctionAndParameters() + if len(parameters) != 4 { + return shim.Error("Wrong number of arguments supplied. Expected: ") + } + + // create the swap + swapID := "swap" + string(parameters[0]) + irsJSON := []byte(parameters[1]) + var irs InterestRateSwap + err := json.Unmarshal(irsJSON, &irs) + if err != nil { + return shim.Error(err.Error()) + } + err = stub.PutState(swapID, irsJSON) + if err != nil { + return shim.Error(err.Error()) + } + + // get the auditing threshold + auditLimit, err := stub.GetState("audit_limit") + if err != nil { + return shim.Error(err.Error()) + } + threshold, err := strconv.Atoi(string(auditLimit)) + if err != nil { + return shim.Error(err.Error()) + } + + // set endorsers + ep, err := statebased.NewStateEP(nil) + if err != nil { + return shim.Error(err.Error()) + } + err = ep.AddOrgs(statebased.RoleTypePeer, parameters[2], parameters[3]) + if err != nil { + return shim.Error(err.Error()) + } + // if the swap principal amount exceeds the audit threshold set in init, the auditor needs to endorse as well + if irs.PrincipalAmount > uint64(threshold) { + fmt.Printf("Adding auditor for swap %s with prinicipal amount %v above threshold %v\n", parameters[0], irs.PrincipalAmount, uint64(threshold)) + err = ep.AddOrgs(statebased.RoleTypePeer, "auditor") + if err != nil { + return shim.Error(err.Error()) + } + } + + // set the endorsement policy for the swap + epBytes, err := ep.Policy() + if err != nil { + return shim.Error(err.Error()) + } + err = stub.SetStateValidationParameter(swapID, epBytes) + if err != nil { + return shim.Error(err.Error()) + } + + // create and set the key for the payment + paymentID := "payment" + string(parameters[0]) + err = stub.PutState(paymentID, []byte("none")) + if err != nil { + return shim.Error(err.Error()) + } + err = stub.SetStateValidationParameter(paymentID, epBytes) + if err != nil { + return shim.Error(err.Error()) + } + + return shim.Success([]byte{}) +} + +// Calculate the payment due for a given swap +func calculatePayment(stub shim.ChaincodeStubInterface) pb.Response { + _, parameters := stub.GetFunctionAndParameters() + if len(parameters) != 1 { + return shim.Error("Wrong number of arguments supplied. Expected: ") + } + + // retrieve swap + swapID := "swap" + parameters[0] + irsJSON, err := stub.GetState(swapID) + if err != nil { + return shim.Error(err.Error()) + } + if irsJSON == nil { + return shim.Error(fmt.Sprintf("Swap %s does not exist", parameters[0])) + } + var irs InterestRateSwap + err = json.Unmarshal(irsJSON, &irs) + if err != nil { + return shim.Error(err.Error()) + } + + // check if the previous payment has been settled + paymentID := "payment" + parameters[0] + paid, err := stub.GetState(paymentID) + if err != nil { + return shim.Error(err.Error()) + } + if paid == nil { + return shim.Error("Unexpected error: payment entry is nil. This should not happen.") + } + if string(paid) != "none" { + return shim.Error("Previous payment has not been settled yet") + } + + // get reference rate + referenceRateBytes, err := stub.GetState("rr" + irs.ReferenceRate) + if err != nil { + return shim.Error(err.Error()) + } + if referenceRateBytes == nil { + return shim.Error(fmt.Sprintf("Reference rate %s not found", irs.ReferenceRate)) + } + referenceRate, err := strconv.Atoi(string(referenceRateBytes)) + if err != nil { + return shim.Error(err.Error()) + } + + // calculate payment + p1 := int((irs.PrincipalAmount * irs.FixedRateBPS) / 100) + p2 := int((irs.PrincipalAmount * (irs.FloatingRateBPS + uint64(referenceRate))) / 100) + payment := strconv.Itoa(p1 - p2) + err = stub.PutState(paymentID, []byte(payment)) + if err != nil { + return shim.Error(err.Error()) + } + + return shim.Success([]byte(payment)) +} + +// Settle the payment for a given swap +func settlePayment(stub shim.ChaincodeStubInterface) pb.Response { + _, parameters := stub.GetFunctionAndParameters() + if len(parameters) != 1 { + return shim.Error("Wrong number of arguments supplied. Expected: ") + } + paymentID := "payment" + parameters[0] + paid, err := stub.GetState(paymentID) + if err != nil { + return shim.Error(err.Error()) + } + if paid == nil { + return shim.Error("Unexpected error: payment entry is nil. This should not happen.") + } + if string(paid) == "none" { + return shim.Error("Payment has already been settled.") + } + err = stub.PutState(paymentID, []byte("none")) + if err != nil { + return shim.Error(err.Error()) + } + return shim.Success([]byte{}) +} + +// Set the reference rate for a given rate provider +func setReferenceRate(stub shim.ChaincodeStubInterface) pb.Response { + _, parameters := stub.GetFunctionAndParameters() + if len(parameters) != 2 { + return shim.Error("Wrong number of arguments supplied. Expected: ") + } + + rrID := "rr" + parameters[0] + err := stub.PutState(rrID, []byte(parameters[1])) + if err != nil { + return shim.Error(err.Error()) + } + return shim.Success([]byte{}) +} + +func main() { + err := shim.Start(new(SwapManager)) + if err != nil { + fmt.Printf("Error starting IRS chaincode: %s", err) + } +} diff --git a/interest_rate_swaps/chaincode/go.mod b/interest_rate_swaps/chaincode/go.mod new file mode 100644 index 0000000..9a1ce33 --- /dev/null +++ b/interest_rate_swaps/chaincode/go.mod @@ -0,0 +1,12 @@ +module github.com/hyperledger/fabric-samples/interest_rate_swaps/chaincode + +go 1.12 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 + github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect + golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + golang.org/x/text v0.3.2 // indirect + google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect +) diff --git a/interest_rate_swaps/chaincode/go.sum b/interest_rate_swaps/chaincode/go.sum new file mode 100644 index 0000000..21d2908 --- /dev/null +++ b/interest_rate_swaps/chaincode/go.sum @@ -0,0 +1,82 @@ +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +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/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85 h1:VEm3tPRTCzq3J/1XpVERh1PbOSnshUVwx2G5s3cLiTw= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20190823162523-04390e015b85/go.mod h1:HZK6PKLWrvdD/t0oSLiyaRaUM6fZ7qjJuOlb0zrn0mo= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022 h1:WzttYAPO5xkQ87ZrxzEhvDZknfarSNu1PZt3NPMTE3Y= +github.com/hyperledger/fabric-protos-go v0.0.0-20190821214336-621b908d5022/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/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-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-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +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/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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/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= diff --git a/interest_rate_swaps/network/.gitignore b/interest_rate_swaps/network/.gitignore new file mode 100644 index 0000000..8150064 --- /dev/null +++ b/interest_rate_swaps/network/.gitignore @@ -0,0 +1,2 @@ +channel-artifacts/ +crypto-config/ diff --git a/interest_rate_swaps/network/configtx.yaml b/interest_rate_swaps/network/configtx.yaml new file mode 100644 index 0000000..2d1c6ca --- /dev/null +++ b/interest_rate_swaps/network/configtx.yaml @@ -0,0 +1,454 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +--- +################################################################################ +# +# Section: Organizations +# +# - This section defines the different organizational identities which will +# be referenced later in the configuration. +# +################################################################################ +Organizations: + + # SampleOrg defines an MSP using the sampleconfig. It should never be used + # in production but may be used as a template for other definitions + - &orderer + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: orderer + + # ID to load the MSP definition as + ID: orderer + + # MSPDir is the filesystem path which contains the MSP configuration + MSPDir: crypto-config/ordererOrganizations/example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('orderer.member')" + Writers: + Type: Signature + Rule: "OR('orderer.member')" + Admins: + Type: Signature + Rule: "OR('orderer.admin')" + + OrdererEndpoints: + - irs-orderer:7050 + + - &partya + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: partya + + # ID to load the MSP definition as + ID: partya + + MSPDir: crypto-config/peerOrganizations/partya.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: OR('partya.admin', 'partya.peer', 'partya.client') + Writers: + Type: Signature + Rule: OR('partya.admin', 'partya.client') + Admins: + Type: Signature + Rule: OR('partya.admin') + Endorsement: + Type: Signature + Rule: "OR('partya.peer')" + + # leave this flag set to true. + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: irs-partya + Port: 7051 + + - &partyb + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: partyb + + # ID to load the MSP definition as + ID: partyb + + MSPDir: crypto-config/peerOrganizations/partyb.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: OR('partyb.admin', 'partyb.peer', 'partyb.client') + Writers: + Type: Signature + Rule: OR('partyb.admin', 'partyb.client') + Admins: + Type: Signature + Rule: OR('partyb.admin') + Endorsement: + Type: Signature + Rule: "OR('partyb.peer')" + + # leave this flag set to true. + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: irs-partyb + Port: 7051 + + - &partyc + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: partyc + + # ID to load the MSP definition as + ID: partyc + + MSPDir: crypto-config/peerOrganizations/partyc.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('partyc.admin', 'partyc.peer', 'partyc.client')" + Writers: + Type: Signature + Rule: "OR('partyc.admin', 'partyc.client')" + Admins: + Type: Signature + Rule: "OR('partyc.admin')" + Endorsement: + Type: Signature + Rule: "OR('partyc.peer')" + + # leave this flag set to true. + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: irs-partyc + Port: 7051 + + + - &auditor + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: auditor + + # ID to load the MSP definition as + ID: auditor + + MSPDir: crypto-config/peerOrganizations/auditor.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: OR('auditor.admin', 'auditor.peer', 'auditor.client') + Writers: + Type: Signature + Rule: OR('auditor.admin', 'auditor.client') + Admins: + Type: Signature + Rule: OR('auditor.admin') + Endorsement: + Type: Signature + Rule: "OR('auditor.peer')" + + # leave this flag set to true. + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: irs-auditor + Port: 7051 + + - &rrprovider + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: rrprovider + + # ID to load the MSP definition as + ID: rrprovider + + MSPDir: crypto-config/peerOrganizations/rrprovider.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('rrprovider.admin', 'rrprovider.peer', 'rrprovider.client')" + Writers: + Type: Signature + Rule: "OR('rrprovider.admin', 'rrprovider.client')" + Admins: + Type: Signature + Rule: "OR('rrprovider.admin')" + Endorsement: + Type: Signature + Rule: "OR('rrprovider.peer')" + + # leave this flag set to true. + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: irs-rrprovider + Port: 7051 + + +################################################################################ +# +# SECTION: Capabilities +# +# - This section defines the capabilities of fabric network. This is a new +# concept as of v1.1.0 and should not be utilized in mixed networks with +# v1.0.x peers and orderers. Capabilities define features which must be +# present in a fabric binary for that binary to safely participate in the +# fabric network. For instance, if a new MSP type is added, newer binaries +# might recognize and validate the signatures from this type, while older +# binaries without this support would be unable to validate those +# transactions. This could lead to different versions of the fabric binaries +# having different world states. Instead, defining a capability for a channel +# informs those binaries without this capability that they must cease +# processing transactions until they have been upgraded. For v1.0.x if any +# capabilities are defined (including a map with all capabilities turned off) +# then the v1.0.x peer will deliberately crash. +# +################################################################################ +Capabilities: + # Channel capabilities apply to both the orderers and the peers and must be + # supported by both. + # Set the value of the capability to true to require it. + Channel: &ChannelCapabilities + # V2_0 capability ensures that orderers and peers behave according + # to v2.0 channel capabilities. Orderers and peers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 capability. + # Prior to enabling V2.0 channel capabilities, ensure that all + # orderers and peers on a channel are at v2.0.0 or later. + V2_0: true + + # Orderer capabilities apply only to the orderers, and may be safely + # used with prior release peers. + # Set the value of the capability to true to require it. + Orderer: &OrdererCapabilities + # V2_0 orderer capability ensures that orderers behave according + # to v2.0 orderer capabilities. Orderers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 orderer capability. + # Prior to enabling V2.0 orderer capabilities, ensure that all + # orderers on channel are at v2.0.0 or later. + V2_0: true + + # Application capabilities apply only to the peer network, and may be safely + # used with prior release orderers. + # Set the value of the capability to true to require it. + Application: &ApplicationCapabilities + # V2_0 application capability ensures that peers behave according + # to v2.0 application capabilities. Peers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 application capability. + # Prior to enabling V2.0 application capabilities, ensure that all + # peers on channel are at v2.0.0 or later. + V2_0: true + +################################################################################ +# +# SECTION: Application +# +# - This section defines the values to encode into a config transaction or +# genesis block for application related parameters +# +################################################################################ +Application: &ApplicationDefaults + + # Organizations is the list of orgs which are defined as participants on + # the application side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Application policies, their canonical path is + # /Channel/Application/ + Policies: + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + LifecycleEndorsement: + Type: ImplicitMeta + Rule: "MAJORITY Endorsement" + Endorsement: + Type: ImplicitMeta + Rule: "MAJORITY Endorsement" + + Capabilities: + <<: *ApplicationCapabilities +################################################################################ +# +# SECTION: Orderer +# +# - This section defines the values to encode into a config transaction or +# genesis block for orderer related parameters +# +################################################################################ +Orderer: &OrdererDefaults + + # Orderer Type: The orderer implementation to start + # Available types are "solo" and "kafka" + OrdererType: solo + + # Batch Timeout: The amount of time to wait before creating a batch + BatchTimeout: 2s + + # Batch Size: Controls the number of messages batched into a block + BatchSize: + + # Max Message Count: The maximum number of messages to permit in a batch + MaxMessageCount: 10 + + # Absolute Max Bytes: The absolute maximum number of bytes allowed for + # the serialized messages in a batch. + AbsoluteMaxBytes: 99 MB + + # Preferred Max Bytes: The preferred maximum number of bytes allowed for + # the serialized messages in a batch. A message larger than the preferred + # max bytes will result in a batch larger than preferred max bytes. + PreferredMaxBytes: 512 KB + + Kafka: + # Brokers: A list of Kafka brokers to which the orderer connects + # NOTE: Use IP:port notation + Brokers: + - 127.0.0.1:9092 + + # Organizations is the list of orgs which are defined as participants on + # the orderer side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Orderer policies, their canonical path is + # /Channel/Orderer/ + Policies: + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + # BlockValidation specifies what signatures must be included in the block + # from the orderer for the peer to validate it. + BlockValidation: + Type: ImplicitMeta + Rule: "ANY Writers" + +################################################################################ +# +# CHANNEL +# +# This section defines the values to encode into a config transaction or +# genesis block for channel related parameters. +# +################################################################################ +Channel: &ChannelDefaults + # Policies defines the set of policies at this level of the config tree + # For Channel policies, their canonical path is + # /Channel/ + Policies: + # Who may invoke the 'Deliver' API + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + # Who may invoke the 'Broadcast' API + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + # By default, who may modify elements at this config level + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + + # Capabilities describes the channel level capabilities, see the + # dedicated Capabilities section elsewhere in this file for a full + # description + Capabilities: + <<: *ChannelCapabilities + +################################################################################ +# +# Profile +# +# - Different configuration profiles may be encoded here to be specified +# as parameters to the configtxgen tool +# +################################################################################ +Profiles: + + IRSNetGenesis: + <<: *ChannelDefaults + Orderer: + <<: *OrdererDefaults + Organizations: + - *orderer + Capabilities: + <<: *OrdererCapabilities + Consortiums: + SampleConsortium: + Organizations: + - *partya + - *partyb + - *partyc + - *rrprovider + - *auditor + IRSChannel: + Consortium: SampleConsortium + <<: *ChannelDefaults + Application: + <<: *ApplicationDefaults + Organizations: + - *partya + - *partyb + - *partyc + - *rrprovider + - *auditor + Capabilities: + <<: *ApplicationCapabilities + + + +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/interest_rate_swaps/network/crypto-config.yaml b/interest_rate_swaps/network/crypto-config.yaml new file mode 100644 index 0000000..b170194 --- /dev/null +++ b/interest_rate_swaps/network/crypto-config.yaml @@ -0,0 +1,51 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +OrdererOrgs: + - Name: orderer + Domain: example.com + Specs: + - Hostname: orderer + +PeerOrgs: + - Name: partya + Domain: partya.example.com + EnableNodeOUs: true + Template: + Count: 1 + Users: + Count: 1 + + - Name: partyb + Domain: partyb.example.com + EnableNodeOUs: true + Template: + Count: 1 + Users: + Count: 1 + + - Name: partyc + Domain: partyc.example.com + EnableNodeOUs: true + Template: + Count: 1 + Users: + Count: 1 + + - Name: auditor + Domain: auditor.example.com + EnableNodeOUs: true + Template: + Count: 1 + Users: + Count: 1 + + - Name: rrprovider + Domain: rrprovider.example.com + EnableNodeOUs: true + Template: + Count: 1 + Users: + Count: 1 diff --git a/interest_rate_swaps/network/docker-compose.yaml b/interest_rate_swaps/network/docker-compose.yaml new file mode 100644 index 0000000..21dc5df --- /dev/null +++ b/interest_rate_swaps/network/docker-compose.yaml @@ -0,0 +1,161 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +version: '2' + +volumes: + orderer.example.com: + peer0.partya.example.com: + peer0.partyb.example.com: + peer0.partyc.example.com: + peer0.auditor.example.com: + peer0.rrprovider.example.com: + +services: + peer-base: + image: hyperledger/fabric-peer:$IMAGE_TAG + environment: + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + - FABRIC_LOGGING_SPEC=INFO + - CORE_PEER_TLS_ENABLED=false + - CORE_PEER_PROFILE_ENABLED=true + - CORE_PEER_ADDRESSAUTODETECT=true + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: peer node start + volumes: + - /var/run/docker.sock:/host/var/run/docker.sock + + orderer: + container_name: irs-orderer + image: hyperledger/fabric-orderer:$IMAGE_TAG + environment: + - FABRIC_LOGGING_SPEC=INFO + - ORDERER_GENERAL_LISTENADDRESS=0.0.0.0 + - ORDERER_GENERAL_BOOTSTRAPMETHOD=file + - ORDERER_GENERAL_BOOTSTRAPFILE=/var/hyperledger/orderer/orderer.genesis.block + - ORDERER_GENERAL_LOCALMSPID=orderer + - ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp + - ORDERER_GENERAL_TLS_ENABLED=false + working_dir: /opt/gopath/src/github.com/hyperledger/fabric + command: orderer + volumes: + - ./channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block + - ./crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/msp:/var/hyperledger/orderer/msp + - orderer.example.com:/var/hyperledger/production/orderer + ports: + - 7050:7050 + + + partya: + container_name: irs-partya + extends: + service: peer-base + environment: + - CORE_PEER_ID=partya.peer0 + - CORE_PEER_ADDRESS=irs-partya:7051 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=irs-partya:7051 + - CORE_PEER_LOCALMSPID=partya + - CORE_CHAINCODE_LOGGING_SHIM=INFO + volumes: + - ./crypto-config/peerOrganizations/partya.example.com/peers/peer0.partya.example.com/msp:/etc/hyperledger/fabric/msp + - peer0.partya.example.com:/var/hyperledger/production + ports: + - 7051:7051 + - 7053:7053 + + partyb: + container_name: irs-partyb + extends: + service: peer-base + environment: + - CORE_PEER_ID=partyb.peer0 + - CORE_PEER_ADDRESS=irs-partyb:7051 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=irs-partyb:7051 + - CORE_PEER_LOCALMSPID=partyb + volumes: + - ./crypto-config/peerOrganizations/partyb.example.com/peers/peer0.partyb.example.com/msp:/etc/hyperledger/fabric/msp + - peer0.partyb.example.com:/var/hyperledger/production + ports: + - 8051:7051 + - 8053:7053 + + partyc: + container_name: irs-partyc + extends: + service: peer-base + environment: + - CORE_PEER_ID=partyc.peer0 + - CORE_PEER_ADDRESS=irs-partyc:7051 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=irs-partyc:7051 + - CORE_PEER_LOCALMSPID=partyc + volumes: + - ./crypto-config/peerOrganizations/partyc.example.com/peers/peer0.partyc.example.com/msp:/etc/hyperledger/fabric/msp + - peer0.partyc.example.com:/var/hyperledger/production + ports: + - 9051:7051 + - 9053:7053 + + auditor: + container_name: irs-auditor + extends: + service: peer-base + environment: + - CORE_PEER_ID=auditor.peer0 + - CORE_PEER_ADDRESS=irs-auditor:7051 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=irs-auditor:7051 + - CORE_PEER_LOCALMSPID=auditor + volumes: + - ./crypto-config/peerOrganizations/auditor.example.com/peers/peer0.auditor.example.com/msp:/etc/hyperledger/fabric/msp + - peer0.auditor.example.com:/var/hyperledger/production + ports: + - 10051:7051 + - 10053:7053 + + rrprovider: + container_name: irs-rrprovider + extends: + service: peer-base + environment: + - CORE_PEER_ID=rrprovider.peer0 + - CORE_PEER_ADDRESS=irs-rrprovider:7051 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=irs-rrprovider:7051 + - CORE_PEER_LOCALMSPID=rrprovider + - CORE_LOGGING_LEVEL=DEBUG + volumes: + - ./crypto-config/peerOrganizations/rrprovider.example.com/peers/peer0.rrprovider.example.com/msp:/etc/hyperledger/fabric/msp + - peer0.rrprovider.example.com:/var/hyperledger/production + ports: + - 11051:7051 + - 11053:7053 + + cli: + container_name: cli + image: hyperledger/fabric-tools:$IMAGE_TAG + tty: true + stdin_open: true + environment: + - GOPATH=/opt/gopath + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + - FABRIC_LOGGING_SPEC=INFO + - CORE_PEER_ID=cli + - CORE_PEER_ADDRESS=irs-partya:7051 + - CORE_PEER_LOCALMSPID=partya + - CORE_PEER_TLS_ENABLED=false + - CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/Admin@partya.example.com/msp + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: /bin/bash + volumes: + - /var/run/docker.sock/:/host/var/run/docker.sock + - ../chaincode/:/opt/gopath/src/irscc + - ./crypto-config:/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ + - ./scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/ + - ./channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts + depends_on: + - orderer + - partya + - partyb + - partyc + - auditor + - rrprovider diff --git a/interest_rate_swaps/network/network.sh b/interest_rate_swaps/network/network.sh new file mode 100755 index 0000000..b87c52d --- /dev/null +++ b/interest_rate_swaps/network/network.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +# This script brings up a test network for the interest-rate swap fabric example. +# It relies on two tools: +# * cryptogen - generates the x509 certificates used to identify and +# authenticate the various components in the network. +# * configtxgen - generates the requisite configuration artifacts for orderer +# bootstrap and channel creation. +# +# Each tool consumes a configuration yaml file, within which we specify the topology +# of our network (cryptogen) and the location of our certificates for various +# configuration operations (configtxgen). Once the tools have been successfully run, +# we are able to launch our network. More detail on the tools and the structure of +# the network will be provided later in this document. For now, let's get going... + +# prepending $PWD/../bin to PATH to ensure we are picking up the correct binaries +# this may be commented out to resolve installed version of tools if desired +export PATH=${PWD}/../../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=${PWD} +export VERBOSE=false + +# Print the usage message +function printHelp() { + echo "Usage: " + echo " start_network.sh [-t ] [-i ] [-v]" + echo " - one of 'up', 'down' or 'generate'" + echo " - 'up' - bring up the network with docker-compose up" + echo " - 'down' - clear the network with docker-compose down" + echo " - 'generate' - generate required certificates and genesis block" + echo " -t - CLI timeout duration in seconds (defaults to 10)" + echo " -i - the tag to be used to launch the network (defaults to \"latest\")" + echo " -v - verbose mode" + echo + echo "Typically, one would first generate the required certificates and " + echo "genesis block, then bring up the network." +} + +# Obtain CONTAINER_IDS and remove them +# TODO Might want to make this optional - could clear other containers +function clearContainers() { + CONTAINER_IDS=$(docker ps -a | awk '($2 ~ /dev-.*irscc.*/) {print $1}') + if [ -z "$CONTAINER_IDS" -o "$CONTAINER_IDS" == " " ]; then + echo "---- No containers available for deletion ----" + else + docker rm -f $CONTAINER_IDS + fi +} + +# Delete any images that were generated as a part of this setup +# specifically the following images are often left behind: +# TODO list generated image naming patterns +function removeUnwantedImages() { + DOCKER_IMAGE_IDS=$(docker images | awk '($1 ~ /dev.*irscc.*/) {print $3}') + if [ -z "$DOCKER_IMAGE_IDS" -o "$DOCKER_IMAGE_IDS" == " " ]; then + echo "---- No images available for deletion ----" + else + docker rmi -f $DOCKER_IMAGE_IDS + fi +} + +# Versions of fabric known not to work with this release of first-network +BLACKLISTED_VERSIONS="^1\.0\. ^1\.1\.0-preview ^1\.1\.0-alpha" + +# Do some basic sanity checking to make sure that the appropriate versions of fabric +# binaries/images are available. In the future, additional checking for the presence +# of go or other items could be added. +function checkPrereqs() { + # Note, we check configtxlator externally because it does not require a config file, and peer in the + # docker image because of FAB-8551 that makes configtxlator return 'development version' in docker + LOCAL_VERSION=$(configtxgen -version | sed -ne 's/ Version: //p') + DOCKER_IMAGE_VERSION=$(docker run --rm hyperledger/fabric-tools:$IMAGETAG peer version | sed -ne 's/ Version: //p' | head -1) + + echo "LOCAL_VERSION=$LOCAL_VERSION" + echo "DOCKER_IMAGE_VERSION=$DOCKER_IMAGE_VERSION" + + if [ "$LOCAL_VERSION" != "$DOCKER_IMAGE_VERSION" ]; then + echo "=================== WARNING ===================" + echo " Local fabric binaries and docker images are " + echo " out of sync. This may cause problems. " + echo "===============================================" + fi + + for UNSUPPORTED_VERSION in $BLACKLISTED_VERSIONS; do + echo "$LOCAL_VERSION" | grep -q $UNSUPPORTED_VERSION + if [ $? -eq 0 ]; then + echo "ERROR! Local Fabric binary version of $LOCAL_VERSION does not match this newer version of BYFN and is unsupported. Either move to a later version of Fabric or checkout an earlier version of fabric-samples." + exit 1 + fi + + echo "$DOCKER_IMAGE_VERSION" | grep -q $UNSUPPORTED_VERSION + if [ $? -eq 0 ]; then + echo "ERROR! Fabric Docker image version of $DOCKER_IMAGE_VERSION does not match this newer version of BYFN and is unsupported. Either move to a later version of Fabric or checkout an earlier version of fabric-samples." + exit 1 + fi + done +} + +# Generate the needed certificates, the genesis block and start the network. +function networkUp() { + checkPrereqs + # generate artifacts if they don't exist + if [ ! -d "crypto-config" ]; then + generateCerts + generateChannelArtifacts + fi + IMAGE_TAG=$IMAGETAG docker-compose -f $COMPOSE_FILE up -d orderer partya partyb partyc auditor rrprovider cli 2>&1 + if [ $? -ne 0 ]; then + echo "ERROR !!!! Unable to start network" + exit 1 + fi + echo Vendoring Go dependencies ... + pushd ../chaincode + GO111MODULE=on go mod vendor + popd + echo Finished vendoring Go dependencies + # now run the end to end script + docker exec cli scripts/script.sh + if [ $? -ne 0 ]; then + echo "ERROR !!!! Test failed" + exit 1 + fi +} + +# Tear down running network +function networkDown() { + # stop org3 containers also in addition to org1 and org2, in case we were running sample to add org3 + docker-compose -f $COMPOSE_FILE down --volumes --remove-orphans + + # Bring down the network, deleting the volumes + #Delete any ledger backups + docker run -v $PWD:/tmp/first-network --rm hyperledger/fabric-tools:$IMAGETAG rm -Rf /tmp/first-network/ledgers-backup + #Cleanup the chaincode containers + clearContainers + #Cleanup images + removeUnwantedImages + # remove orderer block and other channel configuration transactions and certs + rm -rf channel-artifacts/*.block channel-artifacts/*.tx crypto-config +} + +# Generates Org certs using cryptogen tool +function generateCerts() { + which cryptogen + if [ "$?" -ne 0 ]; then + echo "cryptogen tool not found. exiting" + exit 1 + fi + echo "##### Generate certificates using cryptogen tool #########" + + if [ -d "crypto-config" ]; then + rm -Rf crypto-config + fi + cryptogen generate --config=./crypto-config.yaml + res=$? + if [ $res -ne 0 ]; then + echo "Failed to generate certificates..." + exit 1 + fi + echo +} + +# Generate orderer genesis block and channel configuration transaction with configtxgen +function generateChannelArtifacts() { + which configtxgen + if [ "$?" -ne 0 ]; then + echo "configtxgen tool not found. exiting" + exit 1 + fi + + echo "######### Generating Orderer Genesis block ##############" + mkdir channel-artifacts + configtxgen -profile IRSNetGenesis -outputBlock ./channel-artifacts/genesis.block -channelID system-channel + res=$? + if [ $res -ne 0 ]; then + echo "Failed to generate orderer genesis block..." + exit 1 + fi + echo + echo "### Generating channel configuration transaction 'channel.tx' ###" + configtxgen -profile IRSChannel -outputCreateChannelTx ./channel-artifacts/channel.tx -channelID $CHANNEL_NAME + res=$? + if [ $res -ne 0 ]; then + echo "Failed to generate channel configuration transaction..." + exit 1 + fi +} + +CHANNEL_NAME="irs" +COMPOSE_FILE=docker-compose.yaml +COMPOSE_PROJECT_NAME=fabric-irs +# +# default image tag +IMAGETAG="latest" +# Parse commandline args +MODE=$1 +shift +# Determine whether starting, stopping, generating +if [ "$MODE" == "up" ]; then + EXPMODE="Starting" +elif [ "$MODE" == "down" ]; then + EXPMODE="Stopping" +elif [ "$MODE" == "generate" ]; then + EXPMODE="Generating certs and genesis block" +else + printHelp + exit 1 +fi + +while getopts "t:i:v" opt; do + case "$opt" in + t) + CLI_TIMEOUT=$OPTARG + ;; + i) + IMAGETAG=$(go env GOARCH)"-"$OPTARG + ;; + v) + VERBOSE=true + ;; + esac +done + + +# Announce what was requested +echo "${EXPMODE} for channel '${CHANNEL_NAME}'" + +#Create the network using docker compose +if [ "${MODE}" == "up" ]; then + networkUp +elif [ "${MODE}" == "down" ]; then ## Clear the network + networkDown +elif [ "${MODE}" == "generate" ]; then ## Generate Artifacts + generateCerts + generateChannelArtifacts +else + printHelp + exit 1 +fi diff --git a/interest_rate_swaps/network/scripts/check-commit-readiness.sh b/interest_rate_swaps/network/scripts/check-commit-readiness.sh new file mode 100644 index 0000000..f56bf1a --- /dev/null +++ b/interest_rate_swaps/network/scripts/check-commit-readiness.sh @@ -0,0 +1,38 @@ +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +checkCommitReadiness() { + echo "===================== Check the commit readiness of the chaincode definition for ${CORE_PEER_LOCALMSPID} ===================== " + 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 + echo "Attempting to check the commit readiness of the chaincode definition for ${CORE_PEER_LOCALMSPID} ...$(($(date +%s) - starttime)) secs" + set -x + peer lifecycle chaincode checkcommitreadiness -o irs-orderer:7050 --channelID irs --signature-policy "AND(OR('partya.peer','partyb.peer','partyc.peer'), 'auditor.peer')" --name irscc --version 1 --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 for ${CORE_PEER_LOCALMSPID} ===================== " + else + echo "!!!!!!!!!!!!!!! Check commit readiness result for ${CORE_PEER_LOCALMSPID} is INVALID !!!!!!!!!!!!!!!!" + echo "================== ERROR !!! FAILED to execute End-2-End Scenario ==================" + echo + exit 1 + fi +} diff --git a/interest_rate_swaps/network/scripts/script.sh b/interest_rate_swaps/network/scripts/script.sh new file mode 100755 index 0000000..83cd90f --- /dev/null +++ b/interest_rate_swaps/network/scripts/script.sh @@ -0,0 +1,196 @@ +#!/bin/bash + +DELAY="3" +TIMEOUT="10" +VERBOSE="false" +COUNTER=1 +MAX_RETRY=5 + +CC_SRC_PATH="irscc/" + +createChannel() { + CORE_PEER_LOCALMSPID=partya + CORE_PEER_ADDRESS=irs-partya:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/Admin@partya.example.com/msp + echo "===================== Creating channel ===================== " + peer channel create -o irs-orderer:7050 -c irs -f ./channel-artifacts/channel.tx + echo "===================== Channel created ===================== " +} + +joinChannel () { + for org in partya partyb partyc auditor rrprovider + do + CORE_PEER_LOCALMSPID=$org + CORE_PEER_ADDRESS=irs-$org:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/$org.example.com/users/Admin@$org.example.com/msp + echo "===================== Org $org joining channel ===================== " + peer channel join -b irs.block -o irs-orderer:7050 + echo "===================== Channel joined ===================== " + done +} + +packageChaincode() { + CORE_PEER_LOCALMSPID=partya + CORE_PEER_ADDRESS=irs-partya:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/Admin@partya.example.com/msp + echo "===================== Creating chaincode package ===================== " + peer lifecycle chaincode package irscc.tar.gz --path ${CC_SRC_PATH} --lang golang --label irscc_1 + echo "===================== Chaincode packaged ===================== " +} + +installChaincode() { + for org in partya partyb partyc auditor rrprovider + do + CORE_PEER_LOCALMSPID=$org + CORE_PEER_ADDRESS=irs-$org:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/$org.example.com/users/Admin@$org.example.com/msp + echo "===================== Org $org installing chaincode ===================== " + peer lifecycle chaincode install irscc.tar.gz + echo "===================== Org $org chaincode installed ===================== " + done +} + +queryPackage() { + CORE_PEER_LOCALMSPID=partya + CORE_PEER_ADDRESS=irs-partya:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/Admin@partya.example.com/msp + echo "===================== Query chaincode package ID ===================== " + peer lifecycle chaincode queryinstalled >&log.txt + export PACKAGE_ID=`sed -n '/Package/{s/^Package ID: //; s/, Label:.*$//; p;}' log.txt` + echo "packgeID=$PACKAGE_ID" + echo "===================== Query successfull ===================== " +} + +approveChaincode() { + for org in partya partyb partyc auditor rrprovider + do + CORE_PEER_LOCALMSPID=$org + CORE_PEER_ADDRESS=irs-$org:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/$org.example.com/users/Admin@$org.example.com/msp + echo "===================== Approving chaincode definition for $org ===================== " + peer lifecycle chaincode approveformyorg -o irs-orderer:7050 --channelID irs --signature-policy "AND(OR('partya.peer','partyb.peer','partyc.peer'), 'auditor.peer')" --name irscc --version 1 --init-required --sequence 1 --package-id ${PACKAGE_ID} --waitForEvent + echo "===================== Chaincode definition approved ===================== " + done +} + +checkCommitReadiness() { + for org in partya partyb partyc auditor rrprovider + do + export CORE_PEER_LOCALMSPID=$org + export CORE_PEER_ADDRESS=irs-$org:7051 + export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/$org.example.com/users/Admin@$org.example.com/msp + checkCommitReadiness "\"partya\": true" "\"partyb\": true" "\"partyc\": true" "\"auditor\": true" "\"rrprovider\": true" + done +} + +commitChaincode() { + CORE_PEER_LOCALMSPID=partya + CORE_PEER_ADDRESS=irs-partya:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/Admin@partya.example.com/msp + echo "===================== Commiting chaincode definition to channel ===================== " + peer lifecycle chaincode commit -o irs-orderer:7050 --channelID irs --signature-policy "AND(OR('partya.peer','partyb.peer','partyc.peer'), 'auditor.peer')" --name irscc --version 1 --init-required --sequence 1 --peerAddresses irs-partya:7051 --peerAddresses irs-partyb:7051 --peerAddresses irs-partyc:7051 --peerAddresses irs-auditor:7051 --peerAddresses irs-rrprovider:7051 --waitForEvent + echo "===================== Chaincode definition committed ===================== " +} + +initChaincode() { + CORE_PEER_LOCALMSPID=partya + CORE_PEER_ADDRESS=irs-partya:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/Admin@partya.example.com/msp + echo "===================== Initializing chaincode ===================== " + peer chaincode invoke -o irs-orderer:7050 --isInit -C irs --waitForEvent -n irscc --peerAddresses irs-rrprovider:7051 --peerAddresses irs-partya:7051 --peerAddresses irs-partyb:7051 --peerAddresses irs-partyc:7051 --peerAddresses irs-auditor:7051 -c '{"Args":["init","auditor","1000000","rrprovider","myrr"]}' + echo "===================== Chaincode initialized ===================== " +} + +setReferenceRate() { + CORE_PEER_LOCALMSPID=rrprovider + CORE_PEER_ADDRESS=irs-rrprovider:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/rrprovider.example.com/users/User1@rrprovider.example.com/msp + echo "===================== Invoking chaincode ===================== " + peer chaincode invoke -o irs-orderer:7050 -C irs --waitForEvent -n irscc --peerAddresses irs-rrprovider:7051 -c '{"Args":["setReferenceRate","myrr","300"]}' + echo "===================== Chaincode invoked ===================== " +} + +createSwap() { + CORE_PEER_LOCALMSPID=partya + CORE_PEER_ADDRESS=irs-partya:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/User1@partya.example.com/msp + echo "===================== Invoking chaincode ===================== " + peer chaincode invoke -o irs-orderer:7050 -C irs --waitForEvent -n irscc --peerAddresses irs-partya:7051 --peerAddresses irs-partyb:7051 --peerAddresses irs-auditor:7051 -c '{"Args":["createSwap","myswap","{\"StartDate\":\"2018-09-27T15:04:05Z\",\"EndDate\":\"2018-09-30T15:04:05Z\",\"PaymentInterval\":395,\"PrincipalAmount\":100000,\"FixedRateBPS\":400,\"FloatingRateBPS\":500,\"ReferenceRate\":\"myrr\"}", "partya", "partyb"]}' + echo "===================== Chaincode invoked ===================== " +} + +calculatePayment() { + CORE_PEER_LOCALMSPID=partya + CORE_PEER_ADDRESS=irs-partya:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partya.example.com/users/User1@partya.example.com/msp + echo "===================== Invoking chaincode ===================== " + peer chaincode invoke -o irs-orderer:7050 -C irs --waitForEvent -n irscc --peerAddresses irs-partya:7051 --peerAddresses irs-partyb:7051 -c '{"Args":["calculatePayment","myswap"]}' + echo "===================== Chaincode invoked ===================== " +} + +settlePayment() { + CORE_PEER_LOCALMSPID=partyb + CORE_PEER_ADDRESS=irs-partyb:7051 + CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/partyb.example.com/users/User1@partyb.example.com/msp + echo "===================== Invoking chaincode ===================== " + peer chaincode invoke -o irs-orderer:7050 -C irs --waitForEvent -n irscc --peerAddresses irs-partya:7051 --peerAddresses irs-partyb:7051 -c '{"Args":["settlePayment","myswap"]}' + echo "===================== Chaincode invoked ===================== " +} + +## Create channel +sleep 1 +echo "Creating channel..." +createChannel + +## Join all the peers to the channel +echo "Having all peers join the channel..." +joinChannel + +## Package the chaincode +echo "packaging chaincode..." +packageChaincode + +## Query chaincode packageID +echo "Querying packageID..." +installChaincode + +## Install chaincode on all peers +echo "Installing chaincode..." +queryPackage + +# Approve chaincode definition +echo "Approving chaincode..." +approveChaincode + +. scripts/check-commit-readiness.sh + +# Check the commit readiness of the chaincode definition +echo "Checking the commit readiness of the chaincode definition..." +checkCommitReadiness + +# Commit chaincode definition +echo "Committing chaincode definition..." +commitChaincode + +# Init chaincode +echo "Initialize chaincode..." +initChaincode + +echo "Setting myrr reference rate" +sleep 3 +setReferenceRate + +echo "Creating swap between A and B" +createSwap + +echo "Calculate payment information" +calculatePayment + +echo "Mark payment settled" +settlePayment + +echo +echo "========= IRS network sample setup completed =========== " +echo + +exit 0 diff --git a/off_chain_data/README.md b/off_chain_data/README.md new file mode 100644 index 0000000..156649b --- /dev/null +++ b/off_chain_data/README.md @@ -0,0 +1,328 @@ +# Off Chain data + +This sample demonstrates how you can use [Peer channel-based event services](https://hyperledger-fabric.readthedocs.io/en/master/peer_event_services.html) +to replicate the data on your blockchain network to an off chain database. +Using an off chain database allows you to analyze the data from your network or +build a dashboard without degrading the performance of your application. + +This sample uses the [Fabric network event listener](https://hyperledger.github.io/fabric-sdk-node/release-1.4/tutorial-channel-events.html) from the Node.JS Fabric SDK to write data to local instance of +CouchDB. + +## Getting started + +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 + +Use the following command to start the sample network: + +``` +./startFabric.sh +``` + +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 +`asset-transfer-basic` chaincode will be installed on both peers and deployed to +the channel. + +### Configuration + +The configuration for the listener is stored in the `config.json` file: + +``` +{ + "peer_name": "peer0.org1.example.com", + "channelid": "mychannel", + "use_couchdb":true, + "create_history_log":true, + "couchdb_address": "http://localhost:5990" +} +``` + +`peer_name:` is the target peer for the listener. +`channelid:` is the channel name for block events. +`use_couchdb:` If set to true, events will be stored in a local instance of +CouchDB. If set to false, only a local log of events will be stored. +`create_history_log:` If true, a local log file will be created with all of the +block changes. +`couchdb_address:` is the local address for an off chain CouchDB database. + +### Create an instance of CouchDB + +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:2.3.1 +docker start offchaindb +``` + + +### Install dependencies + +You need to install Node.js version 8.9.x to use the sample application code. +Execute the following commands to install the required dependencies: + +``` +npm install +``` + +### Starting the Channel Event Listener + +After we have installed the application dependencies, we can use the Node.js SDK +to create the identity our listener application will use to interact with the +network. Run the following command to enroll the admin user: +``` +node enrollAdmin.js +``` + +You can then run the following command to register and enroll an application +user: + +``` +node registerUser.js +``` + +We can then use our application user to start the block event listener: + +``` +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 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 +processing map called BlockMap for temporary storage and ordering purposes. +`blockEventListener.js` uses `nextblock.txt` to keep track of the latest block +that was retrieved by the listener. The block number in `nextblock.txt` may be +set to a previous block number in order to replay previous blocks. The file +may also be deleted and all blocks will be replayed when the block listener is +started. + +`BlockProcessing.js` runs as a daemon and pulls each block in order from the +BlockMap. It then uses the read-write set of that block to extract the latest +key value data and store it in the database. The configuration blocks of +mychannel did not any data to the database because the blocks did not contain a +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_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. + +**Note:** Leave the blockEventListener.js running in a terminal window. Open a +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 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 `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. + +``` + { + "nextAssetNumber": 100, + "numberAssetsToAdd": 20 + } +``` + +Open a new window and run the following command to add 20 assets to the +blockchain: + +``` +node addAssets.js +``` + +After the assets have been added to the ledger, use the following command to +transfer one of the assets to a new owner: + +``` +node transferAsset.js asset110 james +``` + +Now run the following command to delete the asset that was transferred: + +``` +node deleteAsset.js asset110 +``` + +## Offchain CouchDB storage: + +If you followed the instructions above and set `use_couchdb` to true, +`blockEventListener.js` will create two tables in the local instance of CouchDB. +`blockEventListener.js` is written to create two tables for each channel and for +each chaincode. + +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_basic in this +example. You can navigate to this table using your browser: +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_basic_history +in this example. You can also navigate to this table using your browser: +http://127.0.0.1:5990/mychannel_basic_history/_all_docs + +### 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 +index to support a more complex query. Note that if the `blockEventListener.js` +is not running, the database commands below may fail since the database is only +created when events are received. + +Open a new terminal window and execute the following: + +``` +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 assets (reduce function): + +``` +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 assets in the blockchain +world state, without having to query the blockchain ledger: + +``` +{"rows":[ + {"key":null,"value":19} + ]} +``` + +Execute a new query to retrieve the number of assets by color (map function): + +``` +curl -X GET http://127.0.0.1:5990/mychannel_basic/_design/colorviewdesign/_view/colorview?group=true +``` + +The command will return a list of assets by color from the CouchDB database. + +``` +{"rows":[ + {"key":"blue","value":2}, + {"key":"green","value":2}, + {"key":"purple","value":3}, + {"key":"red","value":4}, + {"key":"white","value":6}, + {"key":"yellow","value":2} + ]} +``` + +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 asset. Execute the +following command to create the index: + +``` +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 asset we transferred and +then deleted: + +``` +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 asset that was created, +transferred, and then removed from the ledger. + +``` +{"docs":[ +{"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":""} + ]} +``` + +## Getting historical data from the network + +You can also use the `blockEventListener.js` program to retrieve historical data +from your network. This allows you to create a database that is up to date with +the latest data from the network or recover any blocks that the program may +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 assets to the +ledger: + +``` +node addAssets.js +``` + +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 assets in your database. + +``` +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` +file that kept track of the latest block read by `blockEventListener.js`: + +``` +rm nextblock.txt +``` + +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 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_basic/_design/colorviewdesign/_view/colorview?reduce=true +``` + +you will be able to see that all of the assets have been added to your +database: + +``` +{"rows":[ +{"key":null,"value":39} +]} +``` + +## Clean up + +If you are finished using the sample application, you can bring down the network +and any accompanying artifacts by running the following command: +``` +./network-clean.sh +``` + +Running the script will complete the following actions: + +* Bring down the Fabric test network. +* Takes down the local CouchDB database. +* 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 `addAssets.json`. diff --git a/off_chain_data/addAssets.js b/off_chain_data/addAssets.js new file mode 100644 index 0000000..1137d5f --- /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/blockEventListener.js b/off_chain_data/blockEventListener.js new file mode 100644 index 0000000..697cf8e --- /dev/null +++ b/off_chain_data/blockEventListener.js @@ -0,0 +1,186 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +/* + +blockEventListener.js is an nodejs application to listen for block events from +a specified channel. + +Configuration is stored in config.json: + +{ + "peer_name": "peer0.org1.example.com", + "channelid": "mychannel", + "use_couchdb":false, + "couchdb_address": "http://localhost:5990" +} + +peer_name: target peer for the listener +channelid: channel name for block events +use_couchdb: if set to true, events will be stored in a local couchdb +couchdb_address: local address for an off chain couchdb database + +Note: If use_couchdb is set to false, only a local log of events will be stored. + +Usage: + +node bockEventListener.js + +The block event listener will log events received to the console and write event blocks to +a log file based on the channelid and chaincode name. + +The event listener stores the next block to retrieve in a file named nextblock.txt. This file +is automatically created and initialized to zero if it does not exist. + +*/ + +'use strict'; + +const { Wallets, Gateway } = require('fabric-network'); +const fs = require('fs'); +const path = require('path'); + +const couchdbutil = require('./couchdbutil.js'); +const blockProcessing = require('./blockProcessing.js'); + +const config = require('./config.json'); +const channelid = config.channelid; +const peer_name = config.peer_name; +const use_couchdb = config.use_couchdb; +const couchdb_address = config.couchdb_address; + +const configPath = path.resolve(__dirname, 'nextblock.txt'); + +const nano = require('nano')(couchdb_address); + +// simple map to hold blocks for processing +class BlockMap { + constructor() { + this.list = [] + } + get(key) { + key = parseInt(key, 10).toString(); + return this.list[`block${key}`]; + } + set(key, value) { + this.list[`block${key}`] = value; + } + remove(key) { + key = parseInt(key, 10).toString(); + delete this.list[`block${key}`]; + } +} + +let ProcessingMap = new BlockMap() + +async function main() { + try { + + // initialize the next block to be 0 + let nextBlock = 0; + + // check to see if there is a next block already defined + if (fs.existsSync(configPath)) { + // read file containing the next block to read + nextBlock = fs.readFileSync(configPath, 'utf8'); + } else { + // store the next block as 0 + fs.writeFileSync(configPath, parseInt(nextBlock, 10)) + } + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const userExists = await wallet.get('appUser'); + if (!userExists) { + console.log('An identity for the user "appUser" does not exist in the wallet'); + console.log('Run the enrollUser.js application before retrying'); + return; + } + + // 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')); + // Create a new gateway for connecting to our peer node. + const gateway = new Gateway(); + await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } }); + + // Get the network (channel) our contract is deployed to. + const network = await gateway.getNetwork('mychannel'); + + const listener = await network.addBlockListener( + async (err, blockNum, block) => { + if (err) { + console.error(err); + return; + } + // Add the block to the processing map by block number + await ProcessingMap.set(block.header.number, block); + + console.log(`Added block ${blockNum} to ProcessingMap`) + }, + // set the starting block for the listener + { filtered: false, startBlock: parseInt(nextBlock, 10) } + ); + + console.log(`Listening for block events, nextblock: ${nextBlock}`); + + // start processing, looking for entries in the ProcessingMap + processPendingBlocks(ProcessingMap); + + } catch (error) { + console.error(`Failed to evaluate transaction: ${error}`); + process.exit(1); + } +} + +// listener function to check for blocks in the ProcessingMap +async function processPendingBlocks(ProcessingMap) { + + setTimeout(async () => { + + // get the next block number from nextblock.txt + let nextBlockNumber = fs.readFileSync(configPath, 'utf8'); + let processBlock; + + do { + + // get the next block to process from the ProcessingMap + processBlock = ProcessingMap.get(nextBlockNumber) + + if (processBlock == undefined) { + break; + } + + try { + await blockProcessing.processBlockEvent(channelid, processBlock, use_couchdb, nano) + } catch (error) { + console.error(`Failed to process block: ${error}`); + } + + // if successful, remove the block from the ProcessingMap + ProcessingMap.remove(nextBlockNumber); + + // increment the next block number to the next block + fs.writeFileSync(configPath, parseInt(nextBlockNumber, 10) + 1) + + // retrive the next block number to process + nextBlockNumber = fs.readFileSync(configPath, 'utf8'); + + } while (true); + + processPendingBlocks(ProcessingMap); + + }, 250); + +} + +main(); diff --git a/off_chain_data/blockProcessing.js b/off_chain_data/blockProcessing.js new file mode 100644 index 0000000..eed61b2 --- /dev/null +++ b/off_chain_data/blockProcessing.js @@ -0,0 +1,216 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const couchdbutil = require('./couchdbutil.js'); + +const configPath = path.resolve(__dirname, 'nextblock.txt'); + +exports.processBlockEvent = async function (channelname, block, use_couchdb, nano) { + + return new Promise((async (resolve, reject) => { + + // reject the block if the block number is not defined + if (block.header.number == undefined) { + reject(new Error('Undefined block number')); + } + + const blockNumber = block.header.number + + console.log(`------------------------------------------------`); + console.log(`Block Number: ${blockNumber}`); + + // reject if the data is not set + if (block.data.data == undefined) { + reject(new Error('Data block is not defined')); + } + + const dataArray = block.data.data; + + // transaction filter for each transaction in dataArray + const txSuccess = block.metadata.metadata[2]; + + for (var dataItem in dataArray) { + + // reject if a timestamp is not set + if (dataArray[dataItem].payload.header.channel_header.timestamp == undefined) { + reject(new Error('Transaction timestamp is not defined')); + } + + // tx may be rejected at commit stage by peers + // only valid transactions (code=0) update the word state and off-chain db + // filter through valid tx, refer below for list of error codes + // https://github.com/hyperledger/fabric-sdk-node/blob/release-1.4/fabric-client/lib/protos/peer/transaction.proto + if (txSuccess[dataItem] !== 0) { + continue; + } + + const timestamp = dataArray[dataItem].payload.header.channel_header.timestamp; + + // continue to next tx if no actions are set + if (dataArray[dataItem].payload.data.actions == undefined) { + continue; + } + + // actions are stored as an array. In Fabric 1.4.3 only one + // action exists per tx so we may simply use actions[0] + // in case Fabric adds support for multiple actions + // a for loop is used for demonstration + const actions = dataArray[dataItem].payload.data.actions; + + // iterate through all actions + for (var actionItem in actions) { + + // reject if a chaincode id is not defined + if (actions[actionItem].payload.chaincode_proposal_payload.input.chaincode_spec.chaincode_id.name == undefined) { + reject(new Error('Chaincode name is not defined')); + } + + const chaincodeID = actions[actionItem].payload.chaincode_proposal_payload.input.chaincode_spec.chaincode_id.name + + // reject if there is no readwrite set + if (actions[actionItem].payload.action.proposal_response_payload.extension.results.ns_rwset == undefined) { + reject(new Error('No readwrite set is defined')); + } + + const rwSet = actions[actionItem].payload.action.proposal_response_payload.extension.results.ns_rwset + + for (var record in rwSet) { + + // ignore lscc events + if (rwSet[record].namespace != 'lscc') { + // create object to store properties + const writeObject = new Object(); + writeObject.blocknumber = blockNumber; + writeObject.chaincodeid = chaincodeID; + writeObject.timestamp = timestamp; + writeObject.values = rwSet[record].rwset.writes; + + console.log(`Transaction Timestamp: ${writeObject.timestamp}`); + console.log(`ChaincodeID: ${writeObject.chaincodeid}`); + console.log(writeObject.values); + + const logfilePath = path.resolve(__dirname, 'nextblock.txt'); + + // send the object to a log file + fs.appendFileSync(channelname + '_' + chaincodeID + '.log', JSON.stringify(writeObject) + "\n"); + + // if couchdb is configured, then write to couchdb + if (use_couchdb) { + try { + await writeValuesToCouchDBP(nano, channelname, writeObject); + } catch (error) { + + } + } + } + }; + }; + }; + + // update the nextblock.txt file to retrieve the next block + fs.writeFileSync(configPath, parseInt(blockNumber, 10) + 1) + + resolve(true); + + })); +} + +async function writeValuesToCouchDBP(nano, channelname, writeObject) { + + return new Promise((async (resolve, reject) => { + + try { + + // define the database for saving block events by key - this emulates world state + const dbname = channelname + '_' + writeObject.chaincodeid; + // define the database for saving all block events - this emulates history + const historydbname = channelname + '_' + writeObject.chaincodeid + '_history'; + // set values to the array of values received + const values = writeObject.values; + + try { + for (var sequence in values) { + let keyvalue = + values[ + sequence + ]; + + if ( + keyvalue.is_delete == + true + ) { + await couchdbutil.deleteRecord( + nano, + dbname, + keyvalue.key + ); + } else { + if ( + isJSON( + keyvalue.value + ) + ) { + // insert or update value by key - this emulates world state behavior + await couchdbutil.writeToCouchDB( + nano, + dbname, + keyvalue.key, + JSON.parse( + keyvalue.value + ) + ); + } + } + + // add additional fields for history + keyvalue.timestamp = + writeObject.timestamp; + keyvalue.blocknumber = parseInt( + writeObject.blocknumber, + 10 + ); + keyvalue.sequence = parseInt( + sequence, + 10 + ); + + await couchdbutil.writeToCouchDB( + nano, + historydbname, + null, + keyvalue + ); + } + } catch (error) { + console.log(error); + reject(error); + } + + } catch (error) { + console.error(`Failed to write to couchdb: ${error}`); + reject(error); + } + + resolve(true); + + })); + +} + +function isJSON(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + return true; +} diff --git a/off_chain_data/config.json b/off_chain_data/config.json new file mode 100644 index 0000000..4df9233 --- /dev/null +++ b/off_chain_data/config.json @@ -0,0 +1,7 @@ +{ + "peer_name": "peer0.org1.example.com", + "channelid": "mychannel", + "use_couchdb":true, + "create_history_log":true, + "couchdb_address": "http://localhost:5990" +} diff --git a/off_chain_data/couchdbutil.js b/off_chain_data/couchdbutil.js new file mode 100644 index 0000000..5e6a532 --- /dev/null +++ b/off_chain_data/couchdbutil.js @@ -0,0 +1,111 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +'use strict'; + +exports.createDatabaseIfNotExists = function (nano, dbname) { + + return new Promise((async (resolve, reject) => { + await nano.db.get(dbname, async function (err, body) { + if (err) { + if (err.statusCode == 404) { + await nano.db.create(dbname, function (err, body) { + if (!err) { + resolve(true); + } else { + reject(err); + } + }); + } else { + reject(err); + } + } else { + resolve(true); + } + }); + })); +} + +exports.writeToCouchDB = async function (nano, dbname, key, value) { + + return new Promise((async (resolve, reject) => { + + try { + await this.createDatabaseIfNotExists(nano, dbname); + } catch (error) { + + } + + const db = nano.use(dbname); + + // If a key is not specified, then this is an insert + if (key == null) { + db.insert(value, async function (err, body, header) { + if (err) { + reject(err); + } + } + ); + } else { + + // If a key is specified, then attempt to retrieve the record by key + db.get(key, async function (err, body) { + // parse the value + const updateValue = value; + // if the record was found, then update the revision to allow the update + if (err == null) { + updateValue._rev = body._rev + } + // update or insert the value + db.insert(updateValue, key, async function (err, body, header) { + if (err) { + reject(err); + } + }); + }); + } + + resolve(true); + + })); +} + + +exports.deleteRecord = async function (nano, dbname, key) { + + return new Promise((async (resolve, reject) => { + + try { + await this.createDatabaseIfNotExists(nano, dbname); + } catch (error) { + + } + + const db = nano.use(dbname); + + // If a key is specified, then attempt to retrieve the record by key + db.get(key, async function (err, body) { + + // if the record was found, then update the revision to allow the update + if (err == null) { + + let revision = body._rev + + // update or insert the value + db.destroy(key, revision, async function (err, body, header) { + if (err) { + reject(err); + } + }); + + } + }); + + resolve(true); + + })); +} diff --git a/off_chain_data/deleteAsset.js b/off_chain_data/deleteAsset.js new file mode 100644 index 0000000..aeede81 --- /dev/null +++ b/off_chain_data/deleteAsset.js @@ -0,0 +1,69 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +/* + * + * deleteAsset.js will delete a specified asset. Example: + * + * $ node deleteAsset.js asset100 + * + * The utility is meant to demonstrate delete block events. + */ + +'use strict'; + +const { Wallets, Gateway } = require('fabric-network'); +const fs = require('fs'); +const path = require('path'); + +const config = require('./config.json'); +const channelid = config.channelid; + +async function main() { + + if (process.argv[2] == undefined) { + console.log("Usage: node deleteAsset AssetId"); + process.exit(1); + } + + const deletekey = process.argv[2]; + + try { + + // 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'); + + await contract.submitTransaction('DeleteAsset', deletekey); + console.log("Deleted asset: " + deletekey); + + await gateway.disconnect(); + + } catch (error) { + console.error(`Failed to submit transaction: ${error}`); + process.exit(1); + } + +} + +main(); diff --git a/off_chain_data/enrollAdmin.js b/off_chain_data/enrollAdmin.js new file mode 100644 index 0000000..9cc7c99 --- /dev/null +++ b/off_chain_data/enrollAdmin.js @@ -0,0 +1,55 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const FabricCAServices = require('fabric-ca-client'); +const { Wallets, X509WalletMixin } = require('fabric-network'); +const fs = require('fs'); +const path = require('path'); + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json'); + let ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new CA client for interacting with the CA. + const caURL = ccp.certificateAuthorities['ca.org1.example.com'].url; + const ca = new FabricCAServices(caURL); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the admin user. + const adminExists = await wallet.get('admin'); + if (adminExists) { + console.log('An identity for the admin user "admin" already exists in the wallet'); + return; + } + + // Enroll the admin user, and import the new identity into the wallet. + const enrollment = await ca.enroll({ enrollmentID: 'admin', enrollmentSecret: 'adminpw' }); + const x509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org1MSP', + type: 'X.509', + }; + await wallet.put('admin', x509Identity); + console.log('Successfully enrolled admin user "admin" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to enroll admin user "admin": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/off_chain_data/network-clean.sh b/off_chain_data/network-clean.sh new file mode 100755 index 0000000..1d24a2a --- /dev/null +++ b/off_chain_data/network-clean.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +# Exit on first error +set -ex + +# Bring the test network down +pushd ../test-network +./network.sh down +popd + +# clean out any old identites in the wallets +rm -rf wallet +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 new file mode 100644 index 0000000..a0d3375 --- /dev/null +++ b/off_chain_data/package.json @@ -0,0 +1,45 @@ +{ + "name": "offchaindata", + "version": "1.0.0", + "description": "Offchain Data application implemented in JavaScript", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.4", + "fabric-network": "^2.2.4" + }, + "devDependencies": { + "chai": "^4.2.0", + "eslint": "^5.9.0", + "mocha": "^5.2.0", + "nyc": "^13.1.0", + "sinon": "^7.1.1", + "sinon-chai": "^3.3.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/off_chain_data/registerUser.js b/off_chain_data/registerUser.js new file mode 100644 index 0000000..1e004e1 --- /dev/null +++ b/off_chain_data/registerUser.js @@ -0,0 +1,75 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Wallets, Gateway, X509WalletMixin } = require('fabric-network'); +const FabricCAServices = require('fabric-ca-client'); +const fs = require('fs'); +const path = require('path'); + +async function main() { + try { + // load the network configuration + const ccpPath = path.resolve(__dirname, '..', 'test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json'); + const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + + // Create a new CA client for interacting with the CA. + const caURL = ccp.certificateAuthorities['ca.org1.example.com'].url; + const ca = new FabricCAServices(caURL); + + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), 'wallet'); + const wallet = await Wallets.newFileSystemWallet(walletPath); + console.log(`Wallet path: ${walletPath}`); + + // Check to see if we've already enrolled the user. + const userExists = await wallet.get('appUser'); + if (userExists) { + console.log('An identity for the user "appUser" already exists in the wallet'); + return; + } + + // Check to see if we've already enrolled the admin user. + const adminIdentity = await wallet.get('admin'); + if (!adminIdentity) { + console.log('An identity for the admin user "admin" does not exist in the wallet'); + console.log('Run the enrollAdmin.js application 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, 'admin'); + + // Register the user, enroll the user, and import the new identity into the wallet. + const secret = await ca.register({ + affiliation: 'org1.department1', + enrollmentID: 'appUser', + role: 'client' + }, adminUser); + const enrollment = await ca.enroll({ + enrollmentID: 'appUser', + enrollmentSecret: secret + }); + const x509Identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes(), + }, + mspId: 'Org1MSP', + type: 'X.509', + }; + await wallet.put('appUser', x509Identity); + console.log('Successfully registered and enrolled admin user "appUser" and imported it into the wallet'); + + } catch (error) { + console.error(`Failed to register user "appUser": ${error}`); + process.exit(1); + } +} + +main(); diff --git a/off_chain_data/startFabric.sh b/off_chain_data/startFabric.sh new file mode 100755 index 0000000..e340bed --- /dev/null +++ b/off_chain_data/startFabric.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +# Exit on first error +set -e pipefail + +starttime=$(date +%s) + +# launch network; create channel and join peer to channel +pushd ../test-network +./network.sh down +./network.sh up createChannel -ca -s couchdb +./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go/ -ccl go + +popd + +cat < CHANGELOG.new << EOF +## "${2}" + +$(git log "$1..HEAD" --oneline | grep -v Merge | sed -e "s/\[\(FAB-[0-9]*\)\]/\[\1\](https:\/\/jira.hyperledger.org\/browse\/\1\)/" -e "s/ \(FAB-[0-9]*\)/ \[\1\](https:\/\/jira.hyperledger.org\/browse\/\1\)/" -e "s/\([0-9|a-z]*\)/* \[\1\](https:\/\/github.com\/hyperledger\/fabric-samples\/commit\/\1)/") + +EOF +cat CHANGELOG.md >> CHANGELOG.new +mv -f CHANGELOG.new CHANGELOG.md diff --git a/test-application/javascript/AppUtil.js b/test-application/javascript/AppUtil.js new file mode 100644 index 0000000..fd2f3fc --- /dev/null +++ b/test-application/javascript/AppUtil.js @@ -0,0 +1,66 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +exports.buildCCPOrg1 = () => { + // 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; +}; + +exports.buildCCPOrg2 = () => { + // 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; +}; + +exports.buildWallet = async (Wallets, walletPath) => { + // Create a new wallet : Note that wallet is for managing identities. + let 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; +}; + +exports.prettyJSONString = (inputString) => { + if (inputString) { + return JSON.stringify(JSON.parse(inputString), null, 2); + } + else { + return inputString; + } +} diff --git a/test-application/javascript/CAUtil.js b/test-application/javascript/CAUtil.js new file mode 100644 index 0000000..10ec734 --- /dev/null +++ b/test-application/javascript/CAUtil.js @@ -0,0 +1,98 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const adminUserId = 'admin'; +const adminUserPasswd = 'adminpw'; + +/** + * + * @param {*} FabricCAServices + * @param {*} ccp + */ +exports.buildCAClient = (FabricCAServices, ccp, caHostName) => { + // 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; +}; + +exports.enrollAdmin = async (caClient, wallet, orgMspId) => { + 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}`); + } +}; + +exports.registerAndEnrollUser = async (caClient, wallet, orgMspId, userId, affiliation) => { + 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: 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}`); + } +}; diff --git a/test-network/.env b/test-network/.env new file mode 100644 index 0000000..eba89f9 --- /dev/null +++ b/test-network/.env @@ -0,0 +1,3 @@ +COMPOSE_PROJECT_NAME=net +IMAGE_TAG=latest +SYS_CHANNEL=system-channel diff --git a/test-network/.gitignore b/test-network/.gitignore new file mode 100644 index 0000000..131b75f --- /dev/null +++ b/test-network/.gitignore @@ -0,0 +1,14 @@ +/channel-artifacts/*.tx +/channel-artifacts/*.block +/ledgers +/ledgers-backup +/channel-artifacts/*.json +/org3-artifacts/crypto-config/* +organizations/fabric-ca/ordererOrg/* +organizations/fabric-ca/org1/* +organizations/fabric-ca/org2/* +organizations/ordererOrganizations/* +organizations/peerOrganizations/* +system-genesis-block/* +*.tar.gz +log.txt diff --git a/test-network/README.md b/test-network/README.md new file mode 100644 index 0000000..418e467 --- /dev/null +++ b/test-network/README.md @@ -0,0 +1,5 @@ +## Running the test network + +You can use the `./network.sh` script to stand up a simple Fabric test network. The test network has two peer organizations with one peer each and a single node raft ordering service. You can also use the `./network.sh` script to create channels and deploy chaincode. For more information, see [Using the Fabric test network](https://hyperledger-fabric.readthedocs.io/en/latest/test_network.html). The test network is being introduced in Fabric v2.0 as the long term replacement for the `first-network` sample. + +Before you can deploy the test network, you need to follow the instructions to [Install the Samples, Binaries and Docker Images](https://hyperledger-fabric.readthedocs.io/en/latest/install.html) in the Hyperledger Fabric documentation. diff --git a/test-network/addOrg3/.env b/test-network/addOrg3/.env new file mode 100644 index 0000000..a6665fe --- /dev/null +++ b/test-network/addOrg3/.env @@ -0,0 +1,2 @@ +COMPOSE_PROJECT_NAME=net +IMAGE_TAG=latest diff --git a/test-network/addOrg3/README.md b/test-network/addOrg3/README.md new file mode 100644 index 0000000..82b1e8a --- /dev/null +++ b/test-network/addOrg3/README.md @@ -0,0 +1,28 @@ +## Adding Org3 to the test network + +You can use the `addOrg3.sh` script to add another organization to the Fabric test network. The `addOrg3.sh` script generates the Org3 crypto material, creates an Org3 organization definition, and adds Org3 to a channel on the test network. + +You first need to run `./network.sh up createChannel` in the `test-network` directory before you can run the `addOrg3.sh` script. + +``` +./network.sh up createChannel +cd addOrg3 +./addOrg3.sh up +``` + +If you used `network.sh` to create a channel other than the default `mychannel`, you need pass that name to the `addorg3.sh` script. +``` +./network.sh up createChannel -c channel1 +cd addOrg3 +./addOrg3.sh up -c channel1 +``` + +You can also re-run the `addOrg3.sh` script to add Org3 to additional channels. +``` +cd .. +./network.sh createChannel -c channel2 +cd addOrg3 +./addOrg3.sh up -c channel2 +``` + +For more information, use `./addOrg3.sh -h` to see the `addOrg3.sh` help text. diff --git a/test-network/addOrg3/addOrg3.sh b/test-network/addOrg3/addOrg3.sh new file mode 100755 index 0000000..b56b92d --- /dev/null +++ b/test-network/addOrg3/addOrg3.sh @@ -0,0 +1,284 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +# This script extends the Hyperledger Fabric test network by adding +# adding a third organization to the network +# + +# prepending $PWD/../bin to PATH to ensure we are picking up the correct binaries +# this may be commented out to resolve installed version of tools if desired +export PATH=${PWD}/../../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=${PWD} +export VERBOSE=false + +. ../scripts/utils.sh + +# Print the usage message +function printHelp () { + echo "Usage: " + echo " addOrg3.sh up|down|generate [-c ] [-t ] [-d ] [-f ] [-s ]" + echo " addOrg3.sh -h|--help (print this message)" + echo " - one of 'up', 'down', or 'generate'" + echo " - 'up' - add org3 to the sample network. You need to bring up the test network and create a channel first." + echo " - 'down' - bring down the test network and org3 nodes" + echo " - 'generate' - generate required certificates and org definition" + echo " -c - test network channel name (defaults to \"mychannel\")" + echo " -ca - Use a CA to generate the crypto material" + echo " -t - CLI timeout duration in seconds (defaults to 10)" + echo " -d - delay duration in seconds (defaults to 3)" + echo " -s - the database backend to use: goleveldb (default) or couchdb" + echo " -i - the tag to be used to launch the network (defaults to \"latest\")" + echo " -cai - the image tag to be used for CA (defaults to \"${CA_IMAGETAG}\")" + echo " -verbose - verbose mode" + echo + echo "Typically, one would first generate the required certificates and " + echo "genesis block, then bring up the network. e.g.:" + echo + echo " addOrg3.sh generate" + echo " addOrg3.sh up" + echo " addOrg3.sh up -c mychannel -s couchdb" + echo " addOrg3.sh down" + echo + echo "Taking all defaults:" + echo " addOrg3.sh up" + echo " addOrg3.sh down" +} + +# We use the cryptogen tool to generate the cryptographic material +# (x509 certs) for the new org. After we run the tool, the certs will +# be put in the organizations folder with org1 and org2 + +# Create Organziation crypto material using cryptogen or CAs +function generateOrg3() { + # Create crypto material using cryptogen + if [ "$CRYPTO" == "cryptogen" ]; then + which cryptogen + if [ "$?" -ne 0 ]; then + fatalln "cryptogen tool not found. exiting" + fi + infoln "Generating certificates using cryptogen tool" + + infoln "Creating Org3 Identities" + + set -x + cryptogen generate --config=org3-crypto.yaml --output="../organizations" + res=$? + { set +x; } 2>/dev/null + if [ $res -ne 0 ]; then + fatalln "Failed to generate certificates..." + fi + + fi + + # Create crypto material using Fabric CA + if [ "$CRYPTO" == "Certificate Authorities" ]; then + fabric-ca-client version > /dev/null 2>&1 + if [[ $? -ne 0 ]]; then + echo "ERROR! fabric-ca-client binary not found.." + echo + echo "Follow the instructions in the Fabric docs to install the Fabric Binaries:" + echo "https://hyperledger-fabric.readthedocs.io/en/latest/install.html" + exit 1 + fi + + infoln "Generating certificates using Fabric CA" + + IMAGE_TAG=${CA_IMAGETAG} docker-compose -f $COMPOSE_FILE_CA_ORG3 up -d 2>&1 + + . fabric-ca/registerEnroll.sh + + sleep 10 + + infoln "Creating Org3 Identities" + createOrg3 + + fi + + infoln "Generating CCP files for Org3" + ./ccp-generate.sh +} + +# Generate channel configuration transaction +function generateOrg3Definition() { + which configtxgen + if [ "$?" -ne 0 ]; then + fatalln "configtxgen tool not found. exiting" + fi + infoln "Generating Org3 organization definition" + export FABRIC_CFG_PATH=$PWD + set -x + configtxgen -printOrg Org3MSP > ../organizations/peerOrganizations/org3.example.com/org3.json + res=$? + { set +x; } 2>/dev/null + if [ $res -ne 0 ]; then + fatalln "Failed to generate Org3 organization definition..." + fi +} + +function Org3Up () { + # start org3 nodes + if [ "${DATABASE}" == "couchdb" ]; then + IMAGE_TAG=${IMAGETAG} docker-compose -f $COMPOSE_FILE_ORG3 -f $COMPOSE_FILE_COUCH_ORG3 up -d 2>&1 + else + IMAGE_TAG=$IMAGETAG docker-compose -f $COMPOSE_FILE_ORG3 up -d 2>&1 + fi + if [ $? -ne 0 ]; then + fatalln "ERROR !!!! Unable to start Org3 network" + fi +} + +# Generate the needed certificates, the genesis block and start the network. +function addOrg3 () { + # If the test network is not up, abort + if [ ! -d ../organizations/ordererOrganizations ]; then + fatalln "ERROR: Please, run ./network.sh up createChannel first." + fi + + # generate artifacts if they don't exist + if [ ! -d "../organizations/peerOrganizations/org3.example.com" ]; then + generateOrg3 + generateOrg3Definition + fi + + infoln "Bringing up Org3 peer" + Org3Up + + # Use the CLI container to create the configuration transaction needed to add + # Org3 to the network + infoln "Generating and submitting config tx to add Org3" + docker exec cli ./scripts/org3-scripts/updateChannelConfig.sh $CHANNEL_NAME $CLI_DELAY $CLI_TIMEOUT $VERBOSE + if [ $? -ne 0 ]; then + fatalln "ERROR !!!! Unable to create config tx" + fi + + infoln "Joining Org3 peers to network" + docker exec cli ./scripts/org3-scripts/joinChannel.sh $CHANNEL_NAME $CLI_DELAY $CLI_TIMEOUT $VERBOSE + if [ $? -ne 0 ]; then + fatalln "ERROR !!!! Unable to join Org3 peers to network" + fi +} + +# Tear down running network +function networkDown () { + cd .. + ./network.sh down +} + + +# Obtain the OS and Architecture string that will be used to select the correct +# native binaries for your platform +OS_ARCH=$(echo "$(uname -s|tr '[:upper:]' '[:lower:]'|sed 's/mingw64_nt.*/windows/')-$(uname -m | sed 's/x86_64/amd64/g')" | awk '{print tolower($0)}') +# timeout duration - the duration the CLI should wait for a response from +# another container before giving up + +# Using crpto vs CA. default is cryptogen +CRYPTO="cryptogen" + +CLI_TIMEOUT=10 +#default for delay +CLI_DELAY=3 +# channel name defaults to "mychannel" +CHANNEL_NAME="mychannel" +# use this as the docker compose couch file +COMPOSE_FILE_COUCH_ORG3=docker/docker-compose-couch-org3.yaml +# use this as the default docker-compose yaml definition +COMPOSE_FILE_ORG3=docker/docker-compose-org3.yaml +# certificate authorities compose file +COMPOSE_FILE_CA_ORG3=docker/docker-compose-ca-org3.yaml +# default image tag +IMAGETAG="latest" +# default ca image tag +CA_IMAGETAG="latest" +# database +DATABASE="leveldb" + +# Parse commandline args + +## Parse mode +if [[ $# -lt 1 ]] ; then + printHelp + exit 0 +else + MODE=$1 + shift +fi + +# parse flags + +while [[ $# -ge 1 ]] ; do + key="$1" + case $key in + -h ) + printHelp + exit 0 + ;; + -c ) + CHANNEL_NAME="$2" + shift + ;; + -ca ) + CRYPTO="Certificate Authorities" + ;; + -t ) + CLI_TIMEOUT="$2" + shift + ;; + -d ) + CLI_DELAY="$2" + shift + ;; + -s ) + DATABASE="$2" + shift + ;; + -i ) + IMAGETAG=$(go env GOARCH)"-""$2" + shift + ;; + -cai ) + CA_IMAGETAG="$2" + shift + ;; + -verbose ) + VERBOSE=true + shift + ;; + * ) + errorln "Unknown flag: $key" + printHelp + exit 1 + ;; + esac + shift +done + + +# Determine whether starting, stopping, restarting or generating for announce +if [ "$MODE" == "up" ]; then + infoln "Adding org3 to channel '${CHANNEL_NAME}' with '${CLI_TIMEOUT}' seconds and CLI delay of '${CLI_DELAY}' seconds and using database '${DATABASE}'" + echo +elif [ "$MODE" == "down" ]; then + EXPMODE="Stopping network" +elif [ "$MODE" == "generate" ]; then + EXPMODE="Generating certs and organization definition for Org3" +else + printHelp + exit 1 +fi + +#Create the network using docker compose +if [ "${MODE}" == "up" ]; then + addOrg3 +elif [ "${MODE}" == "down" ]; then ## Clear the network + networkDown +elif [ "${MODE}" == "generate" ]; then ## Generate Artifacts + generateOrg3 + generateOrg3Definition +else + printHelp + exit 1 +fi diff --git a/test-network/addOrg3/ccp-generate.sh b/test-network/addOrg3/ccp-generate.sh new file mode 100755 index 0000000..a3f254f --- /dev/null +++ b/test-network/addOrg3/ccp-generate.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +function one_line_pem { + echo "`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $1`" +} + +function json_ccp { + local PP=$(one_line_pem $4) + local CP=$(one_line_pem $5) + sed -e "s/\${ORG}/$1/" \ + -e "s/\${P0PORT}/$2/" \ + -e "s/\${CAPORT}/$3/" \ + -e "s#\${PEERPEM}#$PP#" \ + -e "s#\${CAPEM}#$CP#" \ + ccp-template.json +} + +function yaml_ccp { + local PP=$(one_line_pem $4) + local CP=$(one_line_pem $5) + sed -e "s/\${ORG}/$1/" \ + -e "s/\${P0PORT}/$2/" \ + -e "s/\${CAPORT}/$3/" \ + -e "s#\${PEERPEM}#$PP#" \ + -e "s#\${CAPEM}#$CP#" \ + ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g' +} + +ORG=3 +P0PORT=11051 +CAPORT=11054 +PEERPEM=../organizations/peerOrganizations/org3.example.com/tlsca/tlsca.org3.example.com-cert.pem +CAPEM=../organizations/peerOrganizations/org3.example.com/ca/ca.org3.example.com-cert.pem + +echo "$(json_ccp $ORG $P0PORT $CAPORT $PEERPEM $CAPEM)" > ../organizations/peerOrganizations/org3.example.com/connection-org3.json +echo "$(yaml_ccp $ORG $P0PORT $CAPORT $PEERPEM $CAPEM)" > ../organizations/peerOrganizations/org3.example.com/connection-org3.yaml diff --git a/test-network/addOrg3/ccp-template.json b/test-network/addOrg3/ccp-template.json new file mode 100644 index 0000000..2b19d1b --- /dev/null +++ b/test-network/addOrg3/ccp-template.json @@ -0,0 +1,49 @@ +{ + "name": "test-network-org${ORG}", + "version": "1.0.0", + "client": { + "organization": "Org${ORG}", + "connection": { + "timeout": { + "peer": { + "endorser": "300" + } + } + } + }, + "organizations": { + "Org${ORG}": { + "mspid": "Org${ORG}MSP", + "peers": [ + "peer0.org${ORG}.example.com" + ], + "certificateAuthorities": [ + "ca.org${ORG}.example.com" + ] + } + }, + "peers": { + "peer0.org${ORG}.example.com": { + "url": "grpcs://localhost:${P0PORT}", + "tlsCACerts": { + "pem": "${PEERPEM}" + }, + "grpcOptions": { + "ssl-target-name-override": "peer0.org${ORG}.example.com", + "hostnameOverride": "peer0.org${ORG}.example.com" + } + } + }, + "certificateAuthorities": { + "ca.org${ORG}.example.com": { + "url": "https://localhost:${CAPORT}", + "caName": "ca-org${ORG}", + "tlsCACerts": { + "pem": "${CAPEM}" + }, + "httpOptions": { + "verify": false + } + } + } +} diff --git a/test-network/addOrg3/ccp-template.yaml b/test-network/addOrg3/ccp-template.yaml new file mode 100644 index 0000000..7e65965 --- /dev/null +++ b/test-network/addOrg3/ccp-template.yaml @@ -0,0 +1,34 @@ +--- +name: test-network-org${ORG} +version: 1.0.0 +client: + organization: Org${ORG} + connection: + timeout: + peer: + endorser: '300' +organizations: + Org${ORG}: + mspid: Org${ORG}MSP + peers: + - peer0.org${ORG}.example.com + certificateAuthorities: + - ca.org${ORG}.example.com +peers: + peer0.org${ORG}.example.com: + url: grpcs://localhost:${P0PORT} + tlsCACerts: + pem: | + ${PEERPEM} + grpcOptions: + ssl-target-name-override: peer0.org${ORG}.example.com + hostnameOverride: peer0.org${ORG}.example.com +certificateAuthorities: + ca.org${ORG}.example.com: + url: https://localhost:${CAPORT} + caName: ca-org${ORG} + tlsCACerts: + pem: | + ${CAPEM} + httpOptions: + verify: false diff --git a/test-network/addOrg3/configtx.yaml b/test-network/addOrg3/configtx.yaml new file mode 100644 index 0000000..93502f0 --- /dev/null +++ b/test-network/addOrg3/configtx.yaml @@ -0,0 +1,38 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +--- +################################################################################ +# +# Section: Organizations +# +# - This section defines the different organizational identities which will +# be referenced later in the configuration. +# +################################################################################ +Organizations: + - &Org3 + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: Org3MSP + + # ID to load the MSP definition as + ID: Org3MSP + + MSPDir: ../organizations/peerOrganizations/org3.example.com/msp + + Policies: + Readers: + Type: Signature + Rule: "OR('Org3MSP.admin', 'Org3MSP.peer', 'Org3MSP.client')" + Writers: + Type: Signature + Rule: "OR('Org3MSP.admin', 'Org3MSP.client')" + Admins: + Type: Signature + Rule: "OR('Org3MSP.admin')" + Endorsement: + Type: Signature + Rule: "OR('Org3MSP.peer')" diff --git a/test-network/addOrg3/docker/docker-compose-ca-org3.yaml b/test-network/addOrg3/docker/docker-compose-ca-org3.yaml new file mode 100644 index 0000000..46822e8 --- /dev/null +++ b/test-network/addOrg3/docker/docker-compose-ca-org3.yaml @@ -0,0 +1,22 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +version: '2' + +services: + + ca_org3: + image: hyperledger/fabric-ca:$IMAGE_TAG + environment: + - FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server + - FABRIC_CA_SERVER_CA_NAME=ca-org3 + - FABRIC_CA_SERVER_TLS_ENABLED=true + - FABRIC_CA_SERVER_PORT=11054 + ports: + - "11054:11054" + command: sh -c 'fabric-ca-server start -b admin:adminpw -d' + volumes: + - ../fabric-ca/org3:/etc/hyperledger/fabric-ca-server + container_name: ca_org3 diff --git a/test-network/addOrg3/docker/docker-compose-couch-org3.yaml b/test-network/addOrg3/docker/docker-compose-couch-org3.yaml new file mode 100644 index 0000000..8c08a24 --- /dev/null +++ b/test-network/addOrg3/docker/docker-compose-couch-org3.yaml @@ -0,0 +1,39 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +version: '2' + +networks: + test: + +services: + couchdb4: + container_name: couchdb4 + 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: + - COUCHDB_USER=admin + - COUCHDB_PASSWORD=adminpw + # Comment/Uncomment the port mapping if you want to hide/expose the CouchDB service, + # for example map it to utilize Fauxton User Interface in dev environments. + ports: + - "9984:5984" + networks: + - test + + peer0.org3.example.com: + environment: + - CORE_LEDGER_STATE_STATEDATABASE=CouchDB + - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb4:5984 + # The CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME and CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD + # provide the credentials for ledger to connect to CouchDB. The username and password must + # match the username and password set for the associated CouchDB. + - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin + - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=adminpw + depends_on: + - couchdb4 + networks: + - test diff --git a/test-network/addOrg3/docker/docker-compose-org3.yaml b/test-network/addOrg3/docker/docker-compose-org3.yaml new file mode 100644 index 0000000..9727e99 --- /dev/null +++ b/test-network/addOrg3/docker/docker-compose-org3.yaml @@ -0,0 +1,52 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +version: '2' + +volumes: + peer0.org3.example.com: + +networks: + test: + +services: + + peer0.org3.example.com: + container_name: peer0.org3.example.com + image: hyperledger/fabric-peer:$IMAGE_TAG + environment: + #Generic peer variables + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + # the following setting starts chaincode containers on the same + # bridge network as the peers + # https://docs.docker.com/compose/networking/ + - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=${COMPOSE_PROJECT_NAME}_test + - FABRIC_LOGGING_SPEC=INFO + #- FABRIC_LOGGING_SPEC=DEBUG + - CORE_PEER_TLS_ENABLED=true + - CORE_PEER_PROFILE_ENABLED=true + - CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt + - CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key + - CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt + # Peer specific variabes + - CORE_PEER_ID=peer0.org3.example.com + - CORE_PEER_ADDRESS=peer0.org3.example.com:11051 + - CORE_PEER_LISTENADDRESS=0.0.0.0:11051 + - CORE_PEER_CHAINCODEADDRESS=peer0.org3.example.com:11052 + - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:11052 + - CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org3.example.com:11051 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org3.example.com:11051 + - CORE_PEER_LOCALMSPID=Org3MSP + volumes: + - /var/run/docker.sock:/host/var/run/docker.sock + - ../../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/msp:/etc/hyperledger/fabric/msp + - ../../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls:/etc/hyperledger/fabric/tls + - peer0.org3.example.com:/var/hyperledger/production + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: peer node start + ports: + - 11051:11051 + networks: + - test diff --git a/test-network/addOrg3/fabric-ca/org3/fabric-ca-server-config.yaml b/test-network/addOrg3/fabric-ca/org3/fabric-ca-server-config.yaml new file mode 100644 index 0000000..6701693 --- /dev/null +++ b/test-network/addOrg3/fabric-ca/org3/fabric-ca-server-config.yaml @@ -0,0 +1,406 @@ +############################################################################# +# This is a configuration file for the fabric-ca-server command. +# +# COMMAND LINE ARGUMENTS AND ENVIRONMENT VARIABLES +# ------------------------------------------------ +# Each configuration element can be overridden via command line +# arguments or environment variables. The precedence for determining +# the value of each element is as follows: +# 1) command line argument +# Examples: +# a) --port 443 +# To set the listening port +# b) --ca.keyfile ../mykey.pem +# To set the "keyfile" element in the "ca" section below; +# note the '.' separator character. +# 2) environment variable +# Examples: +# a) FABRIC_CA_SERVER_PORT=443 +# To set the listening port +# b) FABRIC_CA_SERVER_CA_KEYFILE="../mykey.pem" +# To set the "keyfile" element in the "ca" section below; +# note the '_' separator character. +# 3) configuration file +# 4) default value (if there is one) +# All default values are shown beside each element below. +# +# FILE NAME ELEMENTS +# ------------------ +# The value of all fields whose name ends with "file" or "files" are +# name or names of other files. +# For example, see "tls.certfile" and "tls.clientauth.certfiles". +# The value of each of these fields can be a simple filename, a +# relative path, or an absolute path. If the value is not an +# absolute path, it is interpretted as being relative to the location +# of this configuration file. +# +############################################################################# + +# Version of config file +version: 1.2.0 + +# Server's listening port (default: 7054) +port: 11054 + +# Enables debug logging (default: false) +debug: false + +# Size limit of an acceptable CRL in bytes (default: 512000) +crlsizelimit: 512000 + +############################################################################# +# TLS section for the server's listening port +# +# The following types are supported for client authentication: NoClientCert, +# RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, +# and RequireAndVerifyClientCert. +# +# Certfiles is a list of root certificate authorities that the server uses +# when verifying client certificates. +############################################################################# +tls: + # Enable TLS (default: false) + enabled: true + # TLS for the server's listening port + certfile: + keyfile: + clientauth: + type: noclientcert + certfiles: + +############################################################################# +# The CA section contains information related to the Certificate Authority +# including the name of the CA, which should be unique for all members +# of a blockchain network. It also includes the key and certificate files +# used when issuing enrollment certificates (ECerts) and transaction +# certificates (TCerts). +# The chainfile (if it exists) contains the certificate chain which +# should be trusted for this CA, where the 1st in the chain is always the +# root CA certificate. +############################################################################# +ca: + # Name of this CA + name: Org3CA + # Key file (is only used to import a private key into BCCSP) + keyfile: + # Certificate file (default: ca-cert.pem) + certfile: + # Chain file + chainfile: + +############################################################################# +# The gencrl REST endpoint is used to generate a CRL that contains revoked +# certificates. This section contains configuration options that are used +# during gencrl request processing. +############################################################################# +crl: + # Specifies expiration for the generated CRL. The number of hours + # specified by this property is added to the UTC time, the resulting time + # is used to set the 'Next Update' date of the CRL. + expiry: 24h + +############################################################################# +# The registry section controls how the fabric-ca-server does two things: +# 1) authenticates enrollment requests which contain a username and password +# (also known as an enrollment ID and secret). +# 2) once authenticated, retrieves the identity's attribute names and +# values which the fabric-ca-server optionally puts into TCerts +# which it issues for transacting on the Hyperledger Fabric blockchain. +# These attributes are useful for making access control decisions in +# chaincode. +# There are two main configuration options: +# 1) The fabric-ca-server is the registry. +# This is true if "ldap.enabled" in the ldap section below is false. +# 2) An LDAP server is the registry, in which case the fabric-ca-server +# calls the LDAP server to perform these tasks. +# This is true if "ldap.enabled" in the ldap section below is true, +# which means this "registry" section is ignored. +############################################################################# +registry: + # Maximum number of times a password/secret can be reused for enrollment + # (default: -1, which means there is no limit) + maxenrollments: -1 + + # Contains identity information which is used when LDAP is disabled + identities: + - name: admin + pass: adminpw + type: client + affiliation: "" + attrs: + hf.Registrar.Roles: "*" + hf.Registrar.DelegateRoles: "*" + hf.Revoker: true + hf.IntermediateCA: true + hf.GenCRL: true + hf.Registrar.Attributes: "*" + hf.AffiliationMgr: true + +############################################################################# +# Database section +# Supported types are: "sqlite3", "postgres", and "mysql". +# The datasource value depends on the type. +# If the type is "sqlite3", the datasource value is a file name to use +# as the database store. Since "sqlite3" is an embedded database, it +# may not be used if you want to run the fabric-ca-server in a cluster. +# To run the fabric-ca-server in a cluster, you must choose "postgres" +# or "mysql". +############################################################################# +db: + type: sqlite3 + datasource: fabric-ca-server.db + tls: + enabled: false + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# LDAP section +# If LDAP is enabled, the fabric-ca-server calls LDAP to: +# 1) authenticate enrollment ID and secret (i.e. username and password) +# for enrollment requests; +# 2) To retrieve identity attributes +############################################################################# +ldap: + # Enables or disables the LDAP client (default: false) + # If this is set to true, the "registry" section is ignored. + enabled: false + # The URL of the LDAP server + url: ldap://:@:/ + # TLS configuration for the client connection to the LDAP server + tls: + certfiles: + client: + certfile: + keyfile: + # Attribute related configuration for mapping from LDAP entries to Fabric CA attributes + attribute: + # 'names' is an array of strings containing the LDAP attribute names which are + # requested from the LDAP server for an LDAP identity's entry + names: ['uid','member'] + # The 'converters' section is used to convert an LDAP entry to the value of + # a fabric CA attribute. + # For example, the following converts an LDAP 'uid' attribute + # whose value begins with 'revoker' to a fabric CA attribute + # named "hf.Revoker" with a value of "true" (because the boolean expression + # evaluates to true). + # converters: + # - name: hf.Revoker + # value: attr("uid") =~ "revoker*" + converters: + - name: + value: + # The 'maps' section contains named maps which may be referenced by the 'map' + # function in the 'converters' section to map LDAP responses to arbitrary values. + # For example, assume a user has an LDAP attribute named 'member' which has multiple + # values which are each a distinguished name (i.e. a DN). For simplicity, assume the + # values of the 'member' attribute are 'dn1', 'dn2', and 'dn3'. + # Further assume the following configuration. + # converters: + # - name: hf.Registrar.Roles + # value: map(attr("member"),"groups") + # maps: + # groups: + # - name: dn1 + # value: peer + # - name: dn2 + # value: client + # The value of the user's 'hf.Registrar.Roles' attribute is then computed to be + # "peer,client,dn3". This is because the value of 'attr("member")' is + # "dn1,dn2,dn3", and the call to 'map' with a 2nd argument of + # "group" replaces "dn1" with "peer" and "dn2" with "client". + maps: + groups: + - name: + value: + +############################################################################# +# Affiliations section. Fabric CA server can be bootstrapped with the +# affiliations specified in this section. Affiliations are specified as maps. +# For example: +# businessunit1: +# department1: +# - team1 +# businessunit2: +# - department2 +# - department3 +# +# Affiliations are hierarchical in nature. In the above example, +# department1 (used as businessunit1.department1) is the child of businessunit1. +# team1 (used as businessunit1.department1.team1) is the child of department1. +# department2 (used as businessunit2.department2) and department3 (businessunit2.department3) +# are children of businessunit2. +# Note: Affiliations are case sensitive except for the non-leaf affiliations +# (like businessunit1, department1, businessunit2) that are specified in the configuration file, +# which are always stored in lower case. +############################################################################# +affiliations: + org1: + - department1 + - department2 + org2: + - department1 + +############################################################################# +# Signing section +# +# The "default" subsection is used to sign enrollment certificates; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +# +# The "ca" profile subsection is used to sign intermediate CA certificates; +# the default expiration ("expiry" field) is "43800h" which is 5 years in hours. +# Note that "isca" is true, meaning that it issues a CA certificate. +# A maxpathlen of 0 means that the intermediate CA cannot issue other +# intermediate CA certificates, though it can still issue end entity certificates. +# (See RFC 5280, section 4.2.1.9) +# +# The "tls" profile subsection is used to sign TLS certificate requests; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +############################################################################# +signing: + default: + usage: + - digital signature + expiry: 8760h + profiles: + ca: + usage: + - cert sign + - crl sign + expiry: 43800h + caconstraint: + isca: true + maxpathlen: 0 + tls: + usage: + - signing + - key encipherment + - server auth + - client auth + - key agreement + expiry: 8760h + +########################################################################### +# Certificate Signing Request (CSR) section. +# This controls the creation of the root CA certificate. +# The expiration for the root CA certificate is configured with the +# "ca.expiry" field below, whose default value is "131400h" which is +# 15 years in hours. +# The pathlength field is used to limit CA certificate hierarchy as described +# in section 4.2.1.9 of RFC 5280. +# Examples: +# 1) No pathlength value means no limit is requested. +# 2) pathlength == 1 means a limit of 1 is requested which is the default for +# a root CA. This means the root CA can issue intermediate CA certificates, +# but these intermediate CAs may not in turn issue other CA certificates +# though they can still issue end entity certificates. +# 3) pathlength == 0 means a limit of 0 is requested; +# this is the default for an intermediate CA, which means it can not issue +# CA certificates though it can still issue end entity certificates. +########################################################################### +csr: + cn: ca.org3.example.com + names: + - C: US + ST: "North Carolina" + L: "Raleigh" + O: org3.example.com + OU: + hosts: + - localhost + - org3.example.com + ca: + expiry: 131400h + pathlength: 1 + +############################################################################# +# BCCSP (BlockChain Crypto Service Provider) section is used to select which +# crypto library implementation to use +############################################################################# +bccsp: + default: SW + sw: + hash: SHA2 + security: 256 + filekeystore: + # The directory used for the software file-based keystore + keystore: msp/keystore + +############################################################################# +# Multi CA section +# +# Each Fabric CA server contains one CA by default. This section is used +# to configure multiple CAs in a single server. +# +# 1) --cacount +# Automatically generate non-default CAs. The names of these +# additional CAs are "ca1", "ca2", ... "caN", where "N" is +# This is particularly useful in a development environment to quickly set up +# multiple CAs. Note that, this config option is not applicable to intermediate CA server +# i.e., Fabric CA server that is started with intermediate.parentserver.url config +# option (-u command line option) +# +# 2) --cafiles +# For each CA config file in the list, generate a separate signing CA. Each CA +# config file in this list MAY contain all of the same elements as are found in +# the server config file except port, debug, and tls sections. +# +# Examples: +# fabric-ca-server start -b admin:adminpw --cacount 2 +# +# fabric-ca-server start -b admin:adminpw --cafiles ca/ca1/fabric-ca-server-config.yaml +# --cafiles ca/ca2/fabric-ca-server-config.yaml +# +############################################################################# + +cacount: + +cafiles: + +############################################################################# +# Intermediate CA section +# +# The relationship between servers and CAs is as follows: +# 1) A single server process may contain or function as one or more CAs. +# This is configured by the "Multi CA section" above. +# 2) Each CA is either a root CA or an intermediate CA. +# 3) Each intermediate CA has a parent CA which is either a root CA or another intermediate CA. +# +# This section pertains to configuration of #2 and #3. +# If the "intermediate.parentserver.url" property is set, +# then this is an intermediate CA with the specified parent +# CA. +# +# parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll within the server +# +# enrollment section used to enroll intermediate CA with parent CA +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# tls section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: diff --git a/test-network/addOrg3/fabric-ca/registerEnroll.sh b/test-network/addOrg3/fabric-ca/registerEnroll.sh new file mode 100644 index 0000000..e2d49c1 --- /dev/null +++ b/test-network/addOrg3/fabric-ca/registerEnroll.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function createOrg3 { + infoln "Enrolling the CA admin" + mkdir -p ../organizations/peerOrganizations/org3.example.com/ + + export FABRIC_CA_CLIENT_HOME=${PWD}/../organizations/peerOrganizations/org3.example.com/ + + set -x + fabric-ca-client enroll -u https://admin:adminpw@localhost:11054 --caname ca-org3 --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + echo 'NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/localhost-11054-ca-org3.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/localhost-11054-ca-org3.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/localhost-11054-ca-org3.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/localhost-11054-ca-org3.pem + OrganizationalUnitIdentifier: orderer' > ${PWD}/../organizations/peerOrganizations/org3.example.com/msp/config.yaml + + infoln "Registering peer0" + set -x + fabric-ca-client register --caname ca-org3 --id.name peer0 --id.secret peer0pw --id.type peer --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Registering user" + set -x + fabric-ca-client register --caname ca-org3 --id.name user1 --id.secret user1pw --id.type client --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Registering the org admin" + set -x + fabric-ca-client register --caname ca-org3 --id.name org3admin --id.secret org3adminpw --id.type admin --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Generating the peer0 msp" + set -x + fabric-ca-client enroll -u https://peer0:peer0pw@localhost:11054 --caname ca-org3 -M ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/msp --csr.hosts peer0.org3.example.com --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/msp/config.yaml ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/msp/config.yaml + + infoln "Generating the peer0-tls certificates" + set -x + fabric-ca-client enroll -u https://peer0:peer0pw@localhost:11054 --caname ca-org3 -M ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls --enrollment.profile tls --csr.hosts peer0.org3.example.com --csr.hosts localhost --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/tlscacerts/* ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/signcerts/* ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/server.crt + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/keystore/* ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/server.key + + mkdir ${PWD}/../organizations/peerOrganizations/org3.example.com/msp/tlscacerts + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/tlscacerts/* ${PWD}/../organizations/peerOrganizations/org3.example.com/msp/tlscacerts/ca.crt + + mkdir ${PWD}/../organizations/peerOrganizations/org3.example.com/tlsca + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/tlscacerts/* ${PWD}/../organizations/peerOrganizations/org3.example.com/tlsca/tlsca.org3.example.com-cert.pem + + mkdir ${PWD}/../organizations/peerOrganizations/org3.example.com/ca + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/msp/cacerts/* ${PWD}/../organizations/peerOrganizations/org3.example.com/ca/ca.org3.example.com-cert.pem + + infoln "Generating the user msp" + set -x + fabric-ca-client enroll -u https://user1:user1pw@localhost:11054 --caname ca-org3 -M ${PWD}/../organizations/peerOrganizations/org3.example.com/users/User1@org3.example.com/msp --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/msp/config.yaml ${PWD}/../organizations/peerOrganizations/org3.example.com/users/User1@org3.example.com/msp/config.yaml + + infoln "Generating the org admin msp" + set -x + fabric-ca-client enroll -u https://org3admin:org3adminpw@localhost:11054 --caname ca-org3 -M ${PWD}/../organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp --tls.certfiles ${PWD}/fabric-ca/org3/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/../organizations/peerOrganizations/org3.example.com/msp/config.yaml ${PWD}/../organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp/config.yaml +} diff --git a/test-network/addOrg3/org3-crypto.yaml b/test-network/addOrg3/org3-crypto.yaml new file mode 100644 index 0000000..73ae733 --- /dev/null +++ b/test-network/addOrg3/org3-crypto.yaml @@ -0,0 +1,21 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# --------------------------------------------------------------------------- +# "PeerOrgs" - Definition of organizations managing peer nodes +# --------------------------------------------------------------------------- +PeerOrgs: + # --------------------------------------------------------------------------- + # Org3 + # --------------------------------------------------------------------------- + - Name: Org3 + Domain: org3.example.com + EnableNodeOUs: true + Template: + Count: 1 + SANS: + - localhost + Users: + Count: 1 diff --git a/test-network/configtx/configtx.yaml b/test-network/configtx/configtx.yaml new file mode 100644 index 0000000..77db240 --- /dev/null +++ b/test-network/configtx/configtx.yaml @@ -0,0 +1,326 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +--- +################################################################################ +# +# Section: Organizations +# +# - This section defines the different organizational identities which will +# be referenced later in the configuration. +# +################################################################################ +Organizations: + + # SampleOrg defines an MSP using the sampleconfig. It should never be used + # in production but may be used as a template for other definitions + - &OrdererOrg + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: OrdererOrg + + # ID to load the MSP definition as + ID: OrdererMSP + + # MSPDir is the filesystem path which contains the MSP configuration + MSPDir: ../organizations/ordererOrganizations/example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('OrdererMSP.member')" + Writers: + Type: Signature + Rule: "OR('OrdererMSP.member')" + Admins: + Type: Signature + Rule: "OR('OrdererMSP.admin')" + + OrdererEndpoints: + - orderer.example.com:7050 + + - &Org1 + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: Org1MSP + + # ID to load the MSP definition as + ID: Org1MSP + + MSPDir: ../organizations/peerOrganizations/org1.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('Org1MSP.admin', 'Org1MSP.peer', 'Org1MSP.client')" + Writers: + Type: Signature + Rule: "OR('Org1MSP.admin', 'Org1MSP.client')" + Admins: + Type: Signature + Rule: "OR('Org1MSP.admin')" + Endorsement: + Type: Signature + Rule: "OR('Org1MSP.peer')" + + - &Org2 + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: Org2MSP + + # ID to load the MSP definition as + ID: Org2MSP + + MSPDir: ../organizations/peerOrganizations/org2.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('Org2MSP.admin', 'Org2MSP.peer', 'Org2MSP.client')" + Writers: + Type: Signature + Rule: "OR('Org2MSP.admin', 'Org2MSP.client')" + Admins: + Type: Signature + Rule: "OR('Org2MSP.admin')" + Endorsement: + Type: Signature + Rule: "OR('Org2MSP.peer')" + +################################################################################ +# +# SECTION: Capabilities +# +# - This section defines the capabilities of fabric network. This is a new +# concept as of v1.1.0 and should not be utilized in mixed networks with +# v1.0.x peers and orderers. Capabilities define features which must be +# present in a fabric binary for that binary to safely participate in the +# fabric network. For instance, if a new MSP type is added, newer binaries +# might recognize and validate the signatures from this type, while older +# binaries without this support would be unable to validate those +# transactions. This could lead to different versions of the fabric binaries +# having different world states. Instead, defining a capability for a channel +# informs those binaries without this capability that they must cease +# processing transactions until they have been upgraded. For v1.0.x if any +# capabilities are defined (including a map with all capabilities turned off) +# then the v1.0.x peer will deliberately crash. +# +################################################################################ +Capabilities: + # Channel capabilities apply to both the orderers and the peers and must be + # supported by both. + # Set the value of the capability to true to require it. + Channel: &ChannelCapabilities + # V2_0 capability ensures that orderers and peers behave according + # to v2.0 channel capabilities. Orderers and peers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 capability. + # Prior to enabling V2.0 channel capabilities, ensure that all + # orderers and peers on a channel are at v2.0.0 or later. + V2_0: true + + # Orderer capabilities apply only to the orderers, and may be safely + # used with prior release peers. + # Set the value of the capability to true to require it. + Orderer: &OrdererCapabilities + # V2_0 orderer capability ensures that orderers behave according + # to v2.0 orderer capabilities. Orderers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 orderer capability. + # Prior to enabling V2.0 orderer capabilities, ensure that all + # orderers on channel are at v2.0.0 or later. + V2_0: true + + # Application capabilities apply only to the peer network, and may be safely + # used with prior release orderers. + # Set the value of the capability to true to require it. + Application: &ApplicationCapabilities + # V2_0 application capability ensures that peers behave according + # to v2.0 application capabilities. Peers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 application capability. + # Prior to enabling V2.0 application capabilities, ensure that all + # peers on channel are at v2.0.0 or later. + V2_0: true + +################################################################################ +# +# SECTION: Application +# +# - This section defines the values to encode into a config transaction or +# genesis block for application related parameters +# +################################################################################ +Application: &ApplicationDefaults + + # Organizations is the list of orgs which are defined as participants on + # the application side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Application policies, their canonical path is + # /Channel/Application/ + Policies: + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + LifecycleEndorsement: + Type: ImplicitMeta + Rule: "MAJORITY Endorsement" + Endorsement: + Type: ImplicitMeta + Rule: "MAJORITY Endorsement" + + Capabilities: + <<: *ApplicationCapabilities +################################################################################ +# +# SECTION: Orderer +# +# - This section defines the values to encode into a config transaction or +# genesis block for orderer related parameters +# +################################################################################ +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: + - Host: orderer.example.com + Port: 7050 + ClientTLSCert: ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt + ServerTLSCert: ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt + + # Batch Timeout: The amount of time to wait before creating a batch + BatchTimeout: 2s + + # Batch Size: Controls the number of messages batched into a block + BatchSize: + + # Max Message Count: The maximum number of messages to permit in a batch + MaxMessageCount: 10 + + # Absolute Max Bytes: The absolute maximum number of bytes allowed for + # the serialized messages in a batch. + AbsoluteMaxBytes: 99 MB + + # Preferred Max Bytes: The preferred maximum number of bytes allowed for + # the serialized messages in a batch. A message larger than the preferred + # max bytes will result in a batch larger than preferred max bytes. + PreferredMaxBytes: 512 KB + + # Organizations is the list of orgs which are defined as participants on + # the orderer side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Orderer policies, their canonical path is + # /Channel/Orderer/ + Policies: + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + # BlockValidation specifies what signatures must be included in the block + # from the orderer for the peer to validate it. + BlockValidation: + Type: ImplicitMeta + Rule: "ANY Writers" + +################################################################################ +# +# CHANNEL +# +# This section defines the values to encode into a config transaction or +# genesis block for channel related parameters. +# +################################################################################ +Channel: &ChannelDefaults + # Policies defines the set of policies at this level of the config tree + # For Channel policies, their canonical path is + # /Channel/ + Policies: + # Who may invoke the 'Deliver' API + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + # Who may invoke the 'Broadcast' API + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + # By default, who may modify elements at this config level + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + + # Capabilities describes the channel level capabilities, see the + # dedicated Capabilities section elsewhere in this file for a full + # description + Capabilities: + <<: *ChannelCapabilities + +################################################################################ +# +# Profile +# +# - Different configuration profiles may be encoded here to be specified +# as parameters to the configtxgen tool +# +################################################################################ +Profiles: + + TwoOrgsOrdererGenesis: + <<: *ChannelDefaults + Orderer: + <<: *OrdererDefaults + Organizations: + - *OrdererOrg + Capabilities: + <<: *OrdererCapabilities + Consortiums: + SampleConsortium: + Organizations: + - *Org1 + - *Org2 + TwoOrgsChannel: + Consortium: SampleConsortium + <<: *ChannelDefaults + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + - *Org2 + Capabilities: + <<: *ApplicationCapabilities diff --git a/test-network/docker/docker-compose-ca.yaml b/test-network/docker/docker-compose-ca.yaml new file mode 100644 index 0000000..15d0ea0 --- /dev/null +++ b/test-network/docker/docker-compose-ca.yaml @@ -0,0 +1,59 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +version: '2' + +networks: + test: + +services: + + ca_org1: + image: hyperledger/fabric-ca:$IMAGE_TAG + environment: + - FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server + - FABRIC_CA_SERVER_CA_NAME=ca-org1 + - FABRIC_CA_SERVER_TLS_ENABLED=true + - FABRIC_CA_SERVER_PORT=7054 + ports: + - "7054:7054" + command: sh -c 'fabric-ca-server start -b admin:adminpw -d' + volumes: + - ../organizations/fabric-ca/org1:/etc/hyperledger/fabric-ca-server + container_name: ca_org1 + networks: + - test + + ca_org2: + image: hyperledger/fabric-ca:$IMAGE_TAG + environment: + - FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server + - FABRIC_CA_SERVER_CA_NAME=ca-org2 + - FABRIC_CA_SERVER_TLS_ENABLED=true + - FABRIC_CA_SERVER_PORT=8054 + ports: + - "8054:8054" + command: sh -c 'fabric-ca-server start -b admin:adminpw -d' + volumes: + - ../organizations/fabric-ca/org2:/etc/hyperledger/fabric-ca-server + container_name: ca_org2 + networks: + - test + + ca_orderer: + image: hyperledger/fabric-ca:$IMAGE_TAG + environment: + - FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server + - FABRIC_CA_SERVER_CA_NAME=ca-orderer + - FABRIC_CA_SERVER_TLS_ENABLED=true + - FABRIC_CA_SERVER_PORT=9054 + ports: + - "9054:9054" + command: sh -c 'fabric-ca-server start -b admin:adminpw -d' + volumes: + - ../organizations/fabric-ca/ordererOrg:/etc/hyperledger/fabric-ca-server + container_name: ca_orderer + networks: + - test diff --git a/test-network/docker/docker-compose-couch.yaml b/test-network/docker/docker-compose-couch.yaml new file mode 100644 index 0000000..1b84e12 --- /dev/null +++ b/test-network/docker/docker-compose-couch.yaml @@ -0,0 +1,64 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +version: '2' + +networks: + test: + +services: + couchdb0: + container_name: couchdb0 + 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: + - COUCHDB_USER=admin + - COUCHDB_PASSWORD=adminpw + # Comment/Uncomment the port mapping if you want to hide/expose the CouchDB service, + # for example map it to utilize Fauxton User Interface in dev environments. + ports: + - "5984:5984" + networks: + - test + + peer0.org1.example.com: + environment: + - CORE_LEDGER_STATE_STATEDATABASE=CouchDB + - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb0:5984 + # The CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME and CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD + # provide the credentials for ledger to connect to CouchDB. The username and password must + # match the username and password set for the associated CouchDB. + - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin + - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=adminpw + depends_on: + - couchdb0 + + couchdb1: + container_name: couchdb1 + 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: + - COUCHDB_USER=admin + - COUCHDB_PASSWORD=adminpw + # Comment/Uncomment the port mapping if you want to hide/expose the CouchDB service, + # for example map it to utilize Fauxton User Interface in dev environments. + ports: + - "7984:5984" + networks: + - test + + peer0.org2.example.com: + environment: + - CORE_LEDGER_STATE_STATEDATABASE=CouchDB + - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb1:5984 + # The CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME and CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD + # provide the credentials for ledger to connect to CouchDB. The username and password must + # match the username and password set for the associated CouchDB. + - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin + - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=adminpw + depends_on: + - couchdb1 diff --git a/test-network/docker/docker-compose-test-net.yaml b/test-network/docker/docker-compose-test-net.yaml new file mode 100644 index 0000000..71acf7f --- /dev/null +++ b/test-network/docker/docker-compose-test-net.yaml @@ -0,0 +1,147 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +version: '2' + +volumes: + orderer.example.com: + peer0.org1.example.com: + peer0.org2.example.com: + +networks: + test: + +services: + + orderer.example.com: + container_name: orderer.example.com + image: hyperledger/fabric-orderer:$IMAGE_TAG + environment: + - FABRIC_LOGGING_SPEC=INFO + - ORDERER_GENERAL_LISTENADDRESS=0.0.0.0 + - ORDERER_GENERAL_LISTENPORT=7050 + - ORDERER_GENERAL_GENESISMETHOD=file + - ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block + - ORDERER_GENERAL_LOCALMSPID=OrdererMSP + - ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp + # enabled TLS + - ORDERER_GENERAL_TLS_ENABLED=true + - ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key + - ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt + - ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt] + - ORDERER_KAFKA_TOPIC_REPLICATIONFACTOR=1 + - ORDERER_KAFKA_VERBOSE=true + - ORDERER_GENERAL_CLUSTER_CLIENTCERTIFICATE=/var/hyperledger/orderer/tls/server.crt + - ORDERER_GENERAL_CLUSTER_CLIENTPRIVATEKEY=/var/hyperledger/orderer/tls/server.key + - ORDERER_GENERAL_CLUSTER_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt] + working_dir: /opt/gopath/src/github.com/hyperledger/fabric + command: orderer + volumes: + - ../system-genesis-block/genesis.block:/var/hyperledger/orderer/orderer.genesis.block + - ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp:/var/hyperledger/orderer/msp + - ../organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/:/var/hyperledger/orderer/tls + - orderer.example.com:/var/hyperledger/production/orderer + ports: + - 7050:7050 + networks: + - test + + peer0.org1.example.com: + container_name: peer0.org1.example.com + image: hyperledger/fabric-peer:$IMAGE_TAG + environment: + #Generic peer variables + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + # the following setting starts chaincode containers on the same + # bridge network as the peers + # https://docs.docker.com/compose/networking/ + - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=${COMPOSE_PROJECT_NAME}_test + - FABRIC_LOGGING_SPEC=INFO + #- FABRIC_LOGGING_SPEC=DEBUG + - CORE_PEER_TLS_ENABLED=true + - CORE_PEER_PROFILE_ENABLED=true + - CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt + - CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key + - CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt + # Peer specific variabes + - CORE_PEER_ID=peer0.org1.example.com + - CORE_PEER_ADDRESS=peer0.org1.example.com:7051 + - CORE_PEER_LISTENADDRESS=0.0.0.0:7051 + - CORE_PEER_CHAINCODEADDRESS=peer0.org1.example.com:7052 + - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052 + - CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org1.example.com:7051 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.example.com:7051 + - CORE_PEER_LOCALMSPID=Org1MSP + volumes: + - /var/run/docker.sock:/host/var/run/docker.sock + - ../organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp:/etc/hyperledger/fabric/msp + - ../organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls:/etc/hyperledger/fabric/tls + - peer0.org1.example.com:/var/hyperledger/production + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: peer node start + ports: + - 7051:7051 + networks: + - test + + peer0.org2.example.com: + container_name: peer0.org2.example.com + image: hyperledger/fabric-peer:$IMAGE_TAG + environment: + #Generic peer variables + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + # the following setting starts chaincode containers on the same + # bridge network as the peers + # https://docs.docker.com/compose/networking/ + - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=${COMPOSE_PROJECT_NAME}_test + - FABRIC_LOGGING_SPEC=INFO + #- FABRIC_LOGGING_SPEC=DEBUG + - CORE_PEER_TLS_ENABLED=true + - CORE_PEER_PROFILE_ENABLED=true + - CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt + - CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key + - CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt + # Peer specific variabes + - CORE_PEER_ID=peer0.org2.example.com + - CORE_PEER_ADDRESS=peer0.org2.example.com:9051 + - CORE_PEER_LISTENADDRESS=0.0.0.0:9051 + - CORE_PEER_CHAINCODEADDRESS=peer0.org2.example.com:9052 + - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:9052 + - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org2.example.com:9051 + - CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org2.example.com:9051 + - CORE_PEER_LOCALMSPID=Org2MSP + volumes: + - /var/run/docker.sock:/host/var/run/docker.sock + - ../organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp:/etc/hyperledger/fabric/msp + - ../organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls:/etc/hyperledger/fabric/tls + - peer0.org2.example.com:/var/hyperledger/production + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: peer node start + ports: + - 9051:9051 + networks: + - test + + cli: + container_name: cli + image: hyperledger/fabric-tools:$IMAGE_TAG + tty: true + stdin_open: true + environment: + - GOPATH=/opt/gopath + - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock + - FABRIC_LOGGING_SPEC=INFO + #- FABRIC_LOGGING_SPEC=DEBUG + working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer + command: /bin/bash + volumes: + - /var/run/:/host/var/run/ + - ../organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations + - ../scripts:/opt/gopath/src/github.com/hyperledger/fabric/peer/scripts/ + depends_on: + - peer0.org1.example.com + - peer0.org2.example.com + networks: + - test diff --git a/test-network/network.sh b/test-network/network.sh new file mode 100755 index 0000000..0768cd6 --- /dev/null +++ b/test-network/network.sh @@ -0,0 +1,522 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +# This script brings up a Hyperledger Fabric network for testing smart contracts +# and applications. The test network consists of two organizations with one +# peer each, and a single node Raft ordering service. Users can also use this +# script to create a channel deploy a chaincode on the channel +# +# prepending $PWD/../bin to PATH to ensure we are picking up the correct binaries +# this may be commented out to resolve installed version of tools if desired +export PATH=${PWD}/../bin:$PATH +export FABRIC_CFG_PATH=${PWD}/configtx +export VERBOSE=false + +. scripts/utils.sh + +# 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 +function clearContainers() { + CONTAINER_IDS=$(docker ps -a | awk '($2 ~ /dev-peer.*/) {print $1}') + if [ -z "$CONTAINER_IDS" -o "$CONTAINER_IDS" == " " ]; then + infoln "No containers available for deletion" + else + docker rm -f $CONTAINER_IDS + fi +} + +# Delete any images that were generated as a part of this setup +# specifically the following images are often left behind: +# This function is called when you bring the network down +function removeUnwantedImages() { + DOCKER_IMAGE_IDS=$(docker images | awk '($1 ~ /dev-peer.*/) {print $3}') + if [ -z "$DOCKER_IMAGE_IDS" -o "$DOCKER_IMAGE_IDS" == " " ]; then + infoln "No images available for deletion" + else + docker rmi -f $DOCKER_IMAGE_IDS + fi +} + +# Versions of fabric known not to work with the test network +NONWORKING_VERSIONS="^1\.0\. ^1\.1\. ^1\.2\. ^1\.3\. ^1\.4\." + +# Do some basic sanity checking to make sure that the appropriate versions of fabric +# binaries/images are available. In the future, additional checking for the presence +# of go or other items could be added. +function checkPrereqs() { + ## Check if your have cloned the peer binaries and configuration files. + peer version > /dev/null 2>&1 + + if [[ $? -ne 0 || ! -d "../config" ]]; then + errorln "Peer binary and configuration files not found.." + errorln + errorln "Follow the instructions in the Fabric docs to install the Fabric Binaries:" + errorln "https://hyperledger-fabric.readthedocs.io/en/latest/install.html" + exit 1 + fi + # use the fabric tools container to see if the samples and binaries match your + # docker images + LOCAL_VERSION=$(peer version | sed -ne 's/ Version: //p') + DOCKER_IMAGE_VERSION=$(docker run --rm hyperledger/fabric-tools:$IMAGETAG peer version | sed -ne 's/ Version: //p' | head -1) + + infoln "LOCAL_VERSION=$LOCAL_VERSION" + infoln "DOCKER_IMAGE_VERSION=$DOCKER_IMAGE_VERSION" + + if [ "$LOCAL_VERSION" != "$DOCKER_IMAGE_VERSION" ]; then + warnln "Local fabric binaries and docker images are out of sync. This may cause problems." + fi + + for UNSUPPORTED_VERSION in $NONWORKING_VERSIONS; do + infoln "$LOCAL_VERSION" | grep -q $UNSUPPORTED_VERSION + if [ $? -eq 0 ]; then + fatalln "Local Fabric binary version of $LOCAL_VERSION does not match the versions supported by the test network." + fi + + infoln "$DOCKER_IMAGE_VERSION" | grep -q $UNSUPPORTED_VERSION + if [ $? -eq 0 ]; then + fatalln "Fabric Docker image version of $DOCKER_IMAGE_VERSION does not match the versions supported by the test network." + fi + done + + ## Check for fabric-ca + if [ "$CRYPTO" == "Certificate Authorities" ]; then + + fabric-ca-client version > /dev/null 2>&1 + if [[ $? -ne 0 ]]; then + errorln "fabric-ca-client binary not found.." + errorln + errorln "Follow the instructions in the Fabric docs to install the Fabric Binaries:" + errorln "https://hyperledger-fabric.readthedocs.io/en/latest/install.html" + exit 1 + fi + CA_LOCAL_VERSION=$(fabric-ca-client version | sed -ne 's/ Version: //p') + CA_DOCKER_IMAGE_VERSION=$(docker run --rm hyperledger/fabric-ca:$CA_IMAGETAG fabric-ca-client version | sed -ne 's/ Version: //p' | head -1) + infoln "CA_LOCAL_VERSION=$CA_LOCAL_VERSION" + infoln "CA_DOCKER_IMAGE_VERSION=$CA_DOCKER_IMAGE_VERSION" + + if [ "$CA_LOCAL_VERSION" != "$CA_DOCKER_IMAGE_VERSION" ]; then + warnln "Local fabric-ca binaries and docker images are out of sync. This may cause problems." + fi + fi +} + +# Before you can bring up a network, each organization needs to generate the crypto +# material that will define that organization on the network. Because Hyperledger +# Fabric is a permissioned blockchain, each node and user on the network needs to +# use certificates and keys to sign and verify its actions. In addition, each user +# needs to belong to an organization that is recognized as a member of the network. +# You can use the Cryptogen tool or Fabric CAs to generate the organization crypto +# material. + +# By default, the sample network uses cryptogen. Cryptogen is a tool that is +# meant for development and testing that can quickly create the certificates and keys +# that can be consumed by a Fabric network. The cryptogen tool consumes a series +# of configuration files for each organization in the "organizations/cryptogen" +# directory. Cryptogen uses the files to generate the crypto material for each +# org in the "organizations" directory. + +# You can also Fabric CAs to generate the crypto material. CAs sign the certificates +# and keys that they generate to create a valid root of trust for each organization. +# The script uses Docker Compose to bring up three CAs, one for each peer organization +# and the ordering organization. The configuration file for creating the Fabric CA +# servers are in the "organizations/fabric-ca" directory. Within the same directory, +# the "registerEnroll.sh" script uses the Fabric CA client to create the identities, +# certificates, and MSP folders that are needed to create the test network in the +# "organizations/ordererOrganizations" directory. + +# Create Organization crypto material using cryptogen or CAs +function createOrgs() { + if [ -d "organizations/peerOrganizations" ]; then + rm -Rf organizations/peerOrganizations && rm -Rf organizations/ordererOrganizations + fi + + # Create crypto material using cryptogen + if [ "$CRYPTO" == "cryptogen" ]; then + which cryptogen + if [ "$?" -ne 0 ]; then + fatalln "cryptogen tool not found. exiting" + fi + infoln "Generating certificates using cryptogen tool" + + infoln "Creating Org1 Identities" + + set -x + cryptogen generate --config=./organizations/cryptogen/crypto-config-org1.yaml --output="organizations" + res=$? + { set +x; } 2>/dev/null + if [ $res -ne 0 ]; then + fatalln "Failed to generate certificates..." + fi + + infoln "Creating Org2 Identities" + + set -x + cryptogen generate --config=./organizations/cryptogen/crypto-config-org2.yaml --output="organizations" + res=$? + { set +x; } 2>/dev/null + if [ $res -ne 0 ]; then + fatalln "Failed to generate certificates..." + fi + + infoln "Creating Orderer Org Identities" + + set -x + cryptogen generate --config=./organizations/cryptogen/crypto-config-orderer.yaml --output="organizations" + res=$? + { set +x; } 2>/dev/null + if [ $res -ne 0 ]; then + fatalln "Failed to generate certificates..." + fi + + fi + + # Create crypto material using Fabric CA + if [ "$CRYPTO" == "Certificate Authorities" ]; then + infoln "Generating certificates using Fabric CA" + + IMAGE_TAG=${CA_IMAGETAG} docker-compose -f $COMPOSE_FILE_CA up -d 2>&1 + + . organizations/fabric-ca/registerEnroll.sh + + while : + do + if [ ! -f "organizations/fabric-ca/org1/tls-cert.pem" ]; then + sleep 1 + else + break + fi + done + + infoln "Creating Org1 Identities" + + createOrg1 + + infoln "Creating Org2 Identities" + + createOrg2 + + infoln "Creating Orderer Org Identities" + + createOrderer + + fi + + infoln "Generating CCP files for Org1 and Org2" + ./organizations/ccp-generate.sh +} + +# Once you create the organization crypto material, you need to create the +# genesis block of the orderer system channel. This block is required to bring +# up any orderer nodes and create any application channels. + +# The configtxgen tool is used to create the genesis block. Configtxgen consumes a +# "configtx.yaml" file that contains the definitions for the sample network. The +# genesis block is defined using the "TwoOrgsOrdererGenesis" profile at the bottom +# of the file. This profile defines a sample consortium, "SampleConsortium", +# consisting of our two Peer Orgs. This consortium defines which organizations are +# recognized as members of the network. The peer and ordering organizations are defined +# in the "Profiles" section at the top of the file. As part of each organization +# profile, the file points to a the location of the MSP directory for each member. +# This MSP is used to create the channel MSP that defines the root of trust for +# each organization. In essence, the channel MSP allows the nodes and users to be +# recognized as network members. The file also specifies the anchor peers for each +# peer org. In future steps, this same file is used to create the channel creation +# transaction and the anchor peer updates. +# +# +# If you receive the following warning, it can be safely ignored: +# +# [bccsp] GetDefault -> WARN 001 Before using BCCSP, please call InitFactories(). Falling back to bootBCCSP. +# +# You can ignore the logs regarding intermediate certs, we are not using them in +# this crypto implementation. + +# Generate orderer system channel genesis block. +function createConsortium() { + which configtxgen + if [ "$?" -ne 0 ]; then + fatalln "configtxgen tool not found." + fi + + infoln "Generating Orderer Genesis block" + + # Note: For some unknown reason (at least for now) the block file can't be + # named orderer.genesis.block or the orderer will fail to launch! + set -x + configtxgen -profile TwoOrgsOrdererGenesis -channelID system-channel -outputBlock ./system-genesis-block/genesis.block + res=$? + { set +x; } 2>/dev/null + if [ $res -ne 0 ]; then + fatalln "Failed to generate orderer genesis block..." + fi +} + +# After we create the org crypto material and the system channel genesis block, +# we can now bring up the peers and ordering service. By default, the base +# file for creating the network is "docker-compose-test-net.yaml" in the ``docker`` +# folder. This file defines the environment variables and file mounts that +# point the crypto material and genesis block that were created in earlier. + +# Bring up the peer and orderer nodes using docker compose. +function networkUp() { + checkPrereqs + # generate artifacts if they don't exist + if [ ! -d "organizations/peerOrganizations" ]; then + createOrgs + createConsortium + fi + + COMPOSE_FILES="-f ${COMPOSE_FILE_BASE}" + + if [ "${DATABASE}" == "couchdb" ]; then + COMPOSE_FILES="${COMPOSE_FILES} -f ${COMPOSE_FILE_COUCH}" + fi + + IMAGE_TAG=$IMAGETAG docker-compose ${COMPOSE_FILES} up -d 2>&1 + + docker ps -a + if [ $? -ne 0 ]; then + fatalln "Unable to start network" + fi +} + +# call the script to create the channel, join the peers of org1 and org2, +# and then update the anchor peers for each organization +function createChannel() { + # Bring up the network if it is not already up. + + if [ ! -d "organizations/peerOrganizations" ]; then + infoln "Bringing up network" + networkUp + fi + + # now run the script that creates a channel. This script uses configtxgen once + # more to create the channel creation transaction and the anchor peer updates. + # configtx.yaml is mounted in the cli container, which allows us to use it to + # create the channel artifacts + scripts/createChannel.sh $CHANNEL_NAME $CLI_DELAY $MAX_RETRY $VERBOSE +} + + +## 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 + + if [ $? -ne 0 ]; then + fatalln "Deploying chaincode failed" + fi +} + + +# Tear down running network +function networkDown() { + # stop org3 containers also in addition to org1 and org2, in case we were running sample to add org3 + docker-compose -f $COMPOSE_FILE_BASE -f $COMPOSE_FILE_COUCH -f $COMPOSE_FILE_CA down --volumes --remove-orphans + docker-compose -f $COMPOSE_FILE_COUCH_ORG3 -f $COMPOSE_FILE_ORG3 down --volumes --remove-orphans + # Don't remove the generated artifacts -- note, the ledgers are always removed + if [ "$MODE" != "restart" ]; then + # Bring down the network, deleting the volumes + #Cleanup the chaincode containers + clearContainers + #Cleanup images + removeUnwantedImages + # remove orderer block and other channel configuration transactions and certs + docker run --rm -v $(pwd):/data busybox sh -c 'cd /data && rm -rf system-genesis-block/*.block organizations/peerOrganizations organizations/ordererOrganizations' + ## remove fabric ca artifacts + docker run --rm -v $(pwd):/data busybox sh -c 'cd /data && rm -rf organizations/fabric-ca/org1/msp organizations/fabric-ca/org1/tls-cert.pem organizations/fabric-ca/org1/ca-cert.pem organizations/fabric-ca/org1/IssuerPublicKey organizations/fabric-ca/org1/IssuerRevocationPublicKey organizations/fabric-ca/org1/fabric-ca-server.db' + docker run --rm -v $(pwd):/data busybox sh -c 'cd /data && rm -rf organizations/fabric-ca/org2/msp organizations/fabric-ca/org2/tls-cert.pem organizations/fabric-ca/org2/ca-cert.pem organizations/fabric-ca/org2/IssuerPublicKey organizations/fabric-ca/org2/IssuerRevocationPublicKey organizations/fabric-ca/org2/fabric-ca-server.db' + docker run --rm -v $(pwd):/data busybox sh -c 'cd /data && rm -rf organizations/fabric-ca/ordererOrg/msp organizations/fabric-ca/ordererOrg/tls-cert.pem organizations/fabric-ca/ordererOrg/ca-cert.pem organizations/fabric-ca/ordererOrg/IssuerPublicKey organizations/fabric-ca/ordererOrg/IssuerRevocationPublicKey organizations/fabric-ca/ordererOrg/fabric-ca-server.db' + docker run --rm -v $(pwd):/data busybox sh -c 'cd /data && rm -rf addOrg3/fabric-ca/org3/msp addOrg3/fabric-ca/org3/tls-cert.pem addOrg3/fabric-ca/org3/ca-cert.pem addOrg3/fabric-ca/org3/IssuerPublicKey addOrg3/fabric-ca/org3/IssuerRevocationPublicKey addOrg3/fabric-ca/org3/fabric-ca-server.db' + # remove channel and script artifacts + docker run --rm -v $(pwd):/data busybox sh -c 'cd /data && rm -rf channel-artifacts log.txt *.tar.gz' + fi +} + +# Obtain the OS and Architecture string that will be used to select the correct +# native binaries for your platform, e.g., darwin-amd64 or linux-amd64 +OS_ARCH=$(echo "$(uname -s | tr '[:upper:]' '[:lower:]' | sed 's/mingw64_nt.*/windows/')-$(uname -m | sed 's/x86_64/amd64/g')" | awk '{print tolower($0)}') +# Using crpto vs CA. default is cryptogen +CRYPTO="cryptogen" +# timeout duration - the duration the CLI should wait for a response from +# another container before giving up +MAX_RETRY=5 +# default for delay between commands +CLI_DELAY=3 +# channel name defaults to "mychannel" +CHANNEL_NAME="mychannel" +# chaincode name defaults to "NA" +CC_NAME="NA" +# chaincode path defaults to "NA" +CC_SRC_PATH="NA" +# endorsement policy defaults to "NA". This would allow chaincodes to use the majority default policy. +CC_END_POLICY="NA" +# collection configuration defaults to "NA" +CC_COLL_CONFIG="NA" +# chaincode init function defaults to "NA" +CC_INIT_FCN="NA" +# use this as the default docker-compose yaml definition +COMPOSE_FILE_BASE=docker/docker-compose-test-net.yaml +# docker-compose.yaml file if you are using couchdb +COMPOSE_FILE_COUCH=docker/docker-compose-couch.yaml +# certificate authorities compose file +COMPOSE_FILE_CA=docker/docker-compose-ca.yaml +# use this as the docker compose couch file for org3 +COMPOSE_FILE_COUCH_ORG3=addOrg3/docker/docker-compose-couch-org3.yaml +# use this as the default docker-compose yaml definition for org3 +COMPOSE_FILE_ORG3=addOrg3/docker/docker-compose-org3.yaml +# +# chaincode language defaults to "NA" +CC_SRC_LANGUAGE="NA" +# Chaincode version +CC_VERSION="1.0" +# Chaincode definition sequence +CC_SEQUENCE=1 +# default image tag +IMAGETAG="latest" +# default ca image tag +CA_IMAGETAG="latest" +# default database +DATABASE="leveldb" + +# Parse commandline args + +## Parse mode +if [[ $# -lt 1 ]] ; then + printHelp + exit 0 +else + MODE=$1 + shift +fi + +# parse a createChannel subcommand if used +if [[ $# -ge 1 ]] ; then + key="$1" + if [[ "$key" == "createChannel" ]]; then + export MODE="createChannel" + shift + fi +fi + +# parse flags + +while [[ $# -ge 1 ]] ; do + key="$1" + case $key in + -h ) + printHelp $MODE + exit 0 + ;; + -c ) + CHANNEL_NAME="$2" + shift + ;; + -ca ) + CRYPTO="Certificate Authorities" + ;; + -r ) + MAX_RETRY="$2" + shift + ;; + -d ) + CLI_DELAY="$2" + shift + ;; + -s ) + DATABASE="$2" + shift + ;; + -ccl ) + CC_SRC_LANGUAGE="$2" + shift + ;; + -ccn ) + CC_NAME="$2" + shift + ;; + -ccv ) + CC_VERSION="$2" + shift + ;; + -ccs ) + CC_SEQUENCE="$2" + shift + ;; + -ccp ) + CC_SRC_PATH="$2" + shift + ;; + -ccep ) + CC_END_POLICY="$2" + shift + ;; + -cccg ) + CC_COLL_CONFIG="$2" + shift + ;; + -cci ) + CC_INIT_FCN="$2" + shift + ;; + -i ) + IMAGETAG="$2" + shift + ;; + -cai ) + CA_IMAGETAG="$2" + shift + ;; + -verbose ) + VERBOSE=true + shift + ;; + * ) + errorln "Unknown flag: $key" + printHelp + exit 1 + ;; + esac + shift +done + +# Are we generating crypto material with this command? +if [ ! -d "organizations/peerOrganizations" ]; then + CRYPTO_MODE="with crypto from '${CRYPTO}'" +else + CRYPTO_MODE="" +fi + +# Determine mode of operation and printing out what we asked for +if [ "$MODE" == "up" ]; then + infoln "Starting nodes with CLI timeout of '${MAX_RETRY}' tries and CLI delay of '${CLI_DELAY}' seconds and using database '${DATABASE}' ${CRYPTO_MODE}" +elif [ "$MODE" == "createChannel" ]; then + infoln "Creating channel '${CHANNEL_NAME}'." + infoln "If network is not up, starting nodes with CLI timeout of '${MAX_RETRY}' tries and CLI delay of '${CLI_DELAY}' seconds and using database '${DATABASE} ${CRYPTO_MODE}" +elif [ "$MODE" == "down" ]; then + infoln "Stopping network" +elif [ "$MODE" == "restart" ]; then + infoln "Restarting network" +elif [ "$MODE" == "deployCC" ]; then + infoln "deploying chaincode on channel '${CHANNEL_NAME}'" +else + printHelp + exit 1 +fi + +if [ "${MODE}" == "up" ]; then + networkUp +elif [ "${MODE}" == "createChannel" ]; then + createChannel +elif [ "${MODE}" == "deployCC" ]; then + deployCC +elif [ "${MODE}" == "down" ]; then + networkDown +else + printHelp + exit 1 +fi diff --git a/test-network/organizations/ccp-generate.sh b/test-network/organizations/ccp-generate.sh new file mode 100755 index 0000000..7e091d0 --- /dev/null +++ b/test-network/organizations/ccp-generate.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +function one_line_pem { + echo "`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $1`" +} + +function json_ccp { + local PP=$(one_line_pem $4) + local CP=$(one_line_pem $5) + sed -e "s/\${ORG}/$1/" \ + -e "s/\${P0PORT}/$2/" \ + -e "s/\${CAPORT}/$3/" \ + -e "s#\${PEERPEM}#$PP#" \ + -e "s#\${CAPEM}#$CP#" \ + organizations/ccp-template.json +} + +function yaml_ccp { + local PP=$(one_line_pem $4) + local CP=$(one_line_pem $5) + sed -e "s/\${ORG}/$1/" \ + -e "s/\${P0PORT}/$2/" \ + -e "s/\${CAPORT}/$3/" \ + -e "s#\${PEERPEM}#$PP#" \ + -e "s#\${CAPEM}#$CP#" \ + organizations/ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g' +} + +ORG=1 +P0PORT=7051 +CAPORT=7054 +PEERPEM=organizations/peerOrganizations/org1.example.com/tlsca/tlsca.org1.example.com-cert.pem +CAPEM=organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem + +echo "$(json_ccp $ORG $P0PORT $CAPORT $PEERPEM $CAPEM)" > organizations/peerOrganizations/org1.example.com/connection-org1.json +echo "$(yaml_ccp $ORG $P0PORT $CAPORT $PEERPEM $CAPEM)" > organizations/peerOrganizations/org1.example.com/connection-org1.yaml + +ORG=2 +P0PORT=9051 +CAPORT=8054 +PEERPEM=organizations/peerOrganizations/org2.example.com/tlsca/tlsca.org2.example.com-cert.pem +CAPEM=organizations/peerOrganizations/org2.example.com/ca/ca.org2.example.com-cert.pem + +echo "$(json_ccp $ORG $P0PORT $CAPORT $PEERPEM $CAPEM)" > organizations/peerOrganizations/org2.example.com/connection-org2.json +echo "$(yaml_ccp $ORG $P0PORT $CAPORT $PEERPEM $CAPEM)" > organizations/peerOrganizations/org2.example.com/connection-org2.yaml diff --git a/test-network/organizations/ccp-template.json b/test-network/organizations/ccp-template.json new file mode 100755 index 0000000..e945bfe --- /dev/null +++ b/test-network/organizations/ccp-template.json @@ -0,0 +1,49 @@ +{ + "name": "test-network-org${ORG}", + "version": "1.0.0", + "client": { + "organization": "Org${ORG}", + "connection": { + "timeout": { + "peer": { + "endorser": "300" + } + } + } + }, + "organizations": { + "Org${ORG}": { + "mspid": "Org${ORG}MSP", + "peers": [ + "peer0.org${ORG}.example.com" + ], + "certificateAuthorities": [ + "ca.org${ORG}.example.com" + ] + } + }, + "peers": { + "peer0.org${ORG}.example.com": { + "url": "grpcs://localhost:${P0PORT}", + "tlsCACerts": { + "pem": "${PEERPEM}" + }, + "grpcOptions": { + "ssl-target-name-override": "peer0.org${ORG}.example.com", + "hostnameOverride": "peer0.org${ORG}.example.com" + } + } + }, + "certificateAuthorities": { + "ca.org${ORG}.example.com": { + "url": "https://localhost:${CAPORT}", + "caName": "ca-org${ORG}", + "tlsCACerts": { + "pem": ["${CAPEM}"] + }, + "httpOptions": { + "verify": false + } + } + } +} diff --git a/test-network/organizations/ccp-template.yaml b/test-network/organizations/ccp-template.yaml new file mode 100755 index 0000000..b675c18 --- /dev/null +++ b/test-network/organizations/ccp-template.yaml @@ -0,0 +1,35 @@ +--- +name: test-network-org${ORG} +version: 1.0.0 +client: + organization: Org${ORG} + connection: + timeout: + peer: + endorser: '300' +organizations: + Org${ORG}: + mspid: Org${ORG}MSP + peers: + - peer0.org${ORG}.example.com + certificateAuthorities: + - ca.org${ORG}.example.com +peers: + peer0.org${ORG}.example.com: + url: grpcs://localhost:${P0PORT} + tlsCACerts: + pem: | + ${PEERPEM} + grpcOptions: + ssl-target-name-override: peer0.org${ORG}.example.com + hostnameOverride: peer0.org${ORG}.example.com +certificateAuthorities: + ca.org${ORG}.example.com: + url: https://localhost:${CAPORT} + caName: ca-org${ORG} + tlsCACerts: + pem: + - | + ${CAPEM} + httpOptions: + verify: false diff --git a/test-network/organizations/cryptogen/crypto-config-orderer.yaml b/test-network/organizations/cryptogen/crypto-config-orderer.yaml new file mode 100755 index 0000000..6c5e766 --- /dev/null +++ b/test-network/organizations/cryptogen/crypto-config-orderer.yaml @@ -0,0 +1,22 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# --------------------------------------------------------------------------- +# "OrdererOrgs" - Definition of organizations managing orderer nodes +# --------------------------------------------------------------------------- +OrdererOrgs: + # --------------------------------------------------------------------------- + # Orderer + # --------------------------------------------------------------------------- + - Name: Orderer + Domain: example.com + EnableNodeOUs: true + # --------------------------------------------------------------------------- + # "Specs" - See PeerOrgs for complete description + # --------------------------------------------------------------------------- + Specs: + - Hostname: orderer + SANS: + - localhost diff --git a/test-network/organizations/cryptogen/crypto-config-org1.yaml b/test-network/organizations/cryptogen/crypto-config-org1.yaml new file mode 100755 index 0000000..4073845 --- /dev/null +++ b/test-network/organizations/cryptogen/crypto-config-org1.yaml @@ -0,0 +1,61 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +# --------------------------------------------------------------------------- +# "PeerOrgs" - Definition of organizations managing peer nodes +# --------------------------------------------------------------------------- +PeerOrgs: + # --------------------------------------------------------------------------- + # Org1 + # --------------------------------------------------------------------------- + - Name: Org1 + Domain: org1.example.com + EnableNodeOUs: true + # --------------------------------------------------------------------------- + # "Specs" + # --------------------------------------------------------------------------- + # Uncomment this section to enable the explicit definition of hosts in your + # configuration. Most users will want to use Template, below + # + # Specs is an array of Spec entries. Each Spec entry consists of two fields: + # - Hostname: (Required) The desired hostname, sans the domain. + # - CommonName: (Optional) Specifies the template or explicit override for + # the CN. By default, this is the template: + # + # "{{.Hostname}}.{{.Domain}}" + # + # which obtains its values from the Spec.Hostname and + # Org.Domain, respectively. + # --------------------------------------------------------------------------- + # - Hostname: foo # implicitly "foo.org1.example.com" + # CommonName: foo27.org5.example.com # overrides Hostname-based FQDN set above + # - Hostname: bar + # - Hostname: baz + # --------------------------------------------------------------------------- + # "Template" + # --------------------------------------------------------------------------- + # Allows for the definition of 1 or more hosts that are created sequentially + # from a template. By default, this looks like "peer%d" from 0 to Count-1. + # You may override the number of nodes (Count), the starting index (Start) + # or the template used to construct the name (Hostname). + # + # Note: Template and Specs are not mutually exclusive. You may define both + # sections and the aggregate nodes will be created for you. Take care with + # name collisions + # --------------------------------------------------------------------------- + Template: + Count: 1 + SANS: + - localhost + # Start: 5 + # Hostname: {{.Prefix}}{{.Index}} # default + # --------------------------------------------------------------------------- + # "Users" + # --------------------------------------------------------------------------- + # Count: The number of user accounts _in addition_ to Admin + # --------------------------------------------------------------------------- + Users: + Count: 1 diff --git a/test-network/organizations/cryptogen/crypto-config-org2.yaml b/test-network/organizations/cryptogen/crypto-config-org2.yaml new file mode 100755 index 0000000..6298ff6 --- /dev/null +++ b/test-network/organizations/cryptogen/crypto-config-org2.yaml @@ -0,0 +1,61 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# --------------------------------------------------------------------------- +# "PeerOrgs" - Definition of organizations managing peer nodes +# --------------------------------------------------------------------------- +PeerOrgs: + # --------------------------------------------------------------------------- + # Org2 + # --------------------------------------------------------------------------- + - Name: Org2 + Domain: org2.example.com + EnableNodeOUs: true + # --------------------------------------------------------------------------- + # "Specs" + # --------------------------------------------------------------------------- + # Uncomment this section to enable the explicit definition of hosts in your + # configuration. Most users will want to use Template, below + # + # Specs is an array of Spec entries. Each Spec entry consists of two fields: + # - Hostname: (Required) The desired hostname, sans the domain. + # - CommonName: (Optional) Specifies the template or explicit override for + # the CN. By default, this is the template: + # + # "{{.Hostname}}.{{.Domain}}" + # + # which obtains its values from the Spec.Hostname and + # Org.Domain, respectively. + # --------------------------------------------------------------------------- + # Specs: + # - Hostname: foo # implicitly "foo.org1.example.com" + # CommonName: foo27.org5.example.com # overrides Hostname-based FQDN set above + # - Hostname: bar + # - Hostname: baz + # --------------------------------------------------------------------------- + # "Template" + # --------------------------------------------------------------------------- + # Allows for the definition of 1 or more hosts that are created sequentially + # from a template. By default, this looks like "peer%d" from 0 to Count-1. + # You may override the number of nodes (Count), the starting index (Start) + # or the template used to construct the name (Hostname). + # + # Note: Template and Specs are not mutually exclusive. You may define both + # sections and the aggregate nodes will be created for you. Take care with + # name collisions + # --------------------------------------------------------------------------- + Template: + Count: 1 + SANS: + - localhost + # Start: 5 + # Hostname: {{.Prefix}}{{.Index}} # default + # --------------------------------------------------------------------------- + # "Users" + # --------------------------------------------------------------------------- + # Count: The number of user accounts _in addition_ to Admin + # --------------------------------------------------------------------------- + Users: + Count: 1 diff --git a/test-network/organizations/fabric-ca/registerEnroll.sh b/test-network/organizations/fabric-ca/registerEnroll.sh new file mode 100755 index 0000000..89ab538 --- /dev/null +++ b/test-network/organizations/fabric-ca/registerEnroll.sh @@ -0,0 +1,226 @@ +#!/bin/bash + +function createOrg1() { + infoln "Enrolling the CA admin" + mkdir -p organizations/peerOrganizations/org1.example.com/ + + export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/ + + set -x + fabric-ca-client enroll -u https://admin:adminpw@localhost:7054 --caname ca-org1 --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + echo 'NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/localhost-7054-ca-org1.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/localhost-7054-ca-org1.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/localhost-7054-ca-org1.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/localhost-7054-ca-org1.pem + OrganizationalUnitIdentifier: orderer' >${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml + + infoln "Registering peer0" + set -x + fabric-ca-client register --caname ca-org1 --id.name peer0 --id.secret peer0pw --id.type peer --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Registering user" + set -x + fabric-ca-client register --caname ca-org1 --id.name user1 --id.secret user1pw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Registering the org admin" + set -x + fabric-ca-client register --caname ca-org1 --id.name org1admin --id.secret org1adminpw --id.type admin --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Generating the peer0 msp" + set -x + fabric-ca-client enroll -u https://peer0:peer0pw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp --csr.hosts peer0.org1.example.com --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/config.yaml + + infoln "Generating the peer0-tls certificates" + set -x + fabric-ca-client enroll -u https://peer0:peer0pw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls --enrollment.profile tls --csr.hosts peer0.org1.example.com --csr.hosts localhost --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/tlscacerts/* ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt + cp ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/signcerts/* ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt + cp ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/keystore/* ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key + + mkdir -p ${PWD}/organizations/peerOrganizations/org1.example.com/msp/tlscacerts + cp ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/tlscacerts/* ${PWD}/organizations/peerOrganizations/org1.example.com/msp/tlscacerts/ca.crt + + mkdir -p ${PWD}/organizations/peerOrganizations/org1.example.com/tlsca + cp ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/tlscacerts/* ${PWD}/organizations/peerOrganizations/org1.example.com/tlsca/tlsca.org1.example.com-cert.pem + + mkdir -p ${PWD}/organizations/peerOrganizations/org1.example.com/ca + cp ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/cacerts/* ${PWD}/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem + + infoln "Generating the user msp" + set -x + fabric-ca-client enroll -u https://user1:user1pw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/config.yaml + + infoln "Generating the org admin msp" + set -x + fabric-ca-client enroll -u https://org1admin:org1adminpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/config.yaml +} + +function createOrg2() { + infoln "Enrolling the CA admin" + mkdir -p organizations/peerOrganizations/org2.example.com/ + + export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/ + + set -x + fabric-ca-client enroll -u https://admin:adminpw@localhost:8054 --caname ca-org2 --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + echo 'NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/localhost-8054-ca-org2.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/localhost-8054-ca-org2.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/localhost-8054-ca-org2.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/localhost-8054-ca-org2.pem + OrganizationalUnitIdentifier: orderer' >${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml + + infoln "Registering peer0" + set -x + fabric-ca-client register --caname ca-org2 --id.name peer0 --id.secret peer0pw --id.type peer --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Registering user" + set -x + fabric-ca-client register --caname ca-org2 --id.name user1 --id.secret user1pw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Registering the org admin" + set -x + fabric-ca-client register --caname ca-org2 --id.name org2admin --id.secret org2adminpw --id.type admin --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Generating the peer0 msp" + set -x + fabric-ca-client enroll -u https://peer0:peer0pw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp --csr.hosts peer0.org2.example.com --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp/config.yaml + + infoln "Generating the peer0-tls certificates" + set -x + fabric-ca-client enroll -u https://peer0:peer0pw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls --enrollment.profile tls --csr.hosts peer0.org2.example.com --csr.hosts localhost --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/tlscacerts/* ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt + cp ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/signcerts/* ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/server.crt + cp ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/keystore/* ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/server.key + + mkdir -p ${PWD}/organizations/peerOrganizations/org2.example.com/msp/tlscacerts + cp ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/tlscacerts/* ${PWD}/organizations/peerOrganizations/org2.example.com/msp/tlscacerts/ca.crt + + mkdir -p ${PWD}/organizations/peerOrganizations/org2.example.com/tlsca + cp ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/tlscacerts/* ${PWD}/organizations/peerOrganizations/org2.example.com/tlsca/tlsca.org2.example.com-cert.pem + + mkdir -p ${PWD}/organizations/peerOrganizations/org2.example.com/ca + cp ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/msp/cacerts/* ${PWD}/organizations/peerOrganizations/org2.example.com/ca/ca.org2.example.com-cert.pem + + infoln "Generating the user msp" + set -x + fabric-ca-client enroll -u https://user1:user1pw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/config.yaml + + infoln "Generating the org admin msp" + set -x + fabric-ca-client enroll -u https://org2admin:org2adminpw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/config.yaml +} + +function createOrderer() { + infoln "Enrolling the CA admin" + mkdir -p organizations/ordererOrganizations/example.com + + export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/ordererOrganizations/example.com + + set -x + fabric-ca-client enroll -u https://admin:adminpw@localhost:9054 --caname ca-orderer --tls.certfiles ${PWD}/organizations/fabric-ca/ordererOrg/tls-cert.pem + { set +x; } 2>/dev/null + + echo 'NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/localhost-9054-ca-orderer.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/localhost-9054-ca-orderer.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/localhost-9054-ca-orderer.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/localhost-9054-ca-orderer.pem + OrganizationalUnitIdentifier: orderer' >${PWD}/organizations/ordererOrganizations/example.com/msp/config.yaml + + infoln "Registering orderer" + set -x + fabric-ca-client register --caname ca-orderer --id.name orderer --id.secret ordererpw --id.type orderer --tls.certfiles ${PWD}/organizations/fabric-ca/ordererOrg/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Registering the orderer admin" + set -x + fabric-ca-client register --caname ca-orderer --id.name ordererAdmin --id.secret ordererAdminpw --id.type admin --tls.certfiles ${PWD}/organizations/fabric-ca/ordererOrg/tls-cert.pem + { set +x; } 2>/dev/null + + infoln "Generating the orderer msp" + set -x + fabric-ca-client enroll -u https://orderer:ordererpw@localhost:9054 --caname ca-orderer -M ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp --csr.hosts orderer.example.com --csr.hosts localhost --tls.certfiles ${PWD}/organizations/fabric-ca/ordererOrg/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/ordererOrganizations/example.com/msp/config.yaml ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/config.yaml + + infoln "Generating the orderer-tls certificates" + set -x + fabric-ca-client enroll -u https://orderer:ordererpw@localhost:9054 --caname ca-orderer -M ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls --enrollment.profile tls --csr.hosts orderer.example.com --csr.hosts localhost --tls.certfiles ${PWD}/organizations/fabric-ca/ordererOrg/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/tlscacerts/* ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt + cp ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/signcerts/* ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.crt + cp ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/keystore/* ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/server.key + + mkdir -p ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts + cp ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/tlscacerts/* ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem + + mkdir -p ${PWD}/organizations/ordererOrganizations/example.com/msp/tlscacerts + cp ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/tlscacerts/* ${PWD}/organizations/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem + + infoln "Generating the admin msp" + set -x + fabric-ca-client enroll -u https://ordererAdmin:ordererAdminpw@localhost:9054 --caname ca-orderer -M ${PWD}/organizations/ordererOrganizations/example.com/users/Admin@example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/ordererOrg/tls-cert.pem + { set +x; } 2>/dev/null + + cp ${PWD}/organizations/ordererOrganizations/example.com/msp/config.yaml ${PWD}/organizations/ordererOrganizations/example.com/users/Admin@example.com/msp/config.yaml +} diff --git a/test-network/scripts/configUpdate.sh b/test-network/scripts/configUpdate.sh new file mode 100755 index 0000000..f5d941f --- /dev/null +++ b/test-network/scripts/configUpdate.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# import utils +. scripts/envVar.sh + +# fetchChannelConfig +# Writes the current channel config for a given channel to a JSON file +# NOTE: this must be run in a CLI container since it requires configtxlator +fetchChannelConfig() { + ORG=$1 + CHANNEL=$2 + OUTPUT=$3 + + setGlobals $ORG + + infoln "Fetching the most recent configuration block for the channel" + set -x + peer channel fetch config config_block.pb -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com -c $CHANNEL --tls --cafile $ORDERER_CA + { set +x; } 2>/dev/null + + infoln "Decoding config block to JSON and isolating config to ${OUTPUT}" + set -x + configtxlator proto_decode --input config_block.pb --type common.Block | jq .data.data[0].payload.data.config >"${OUTPUT}" + { set +x; } 2>/dev/null +} + +# createConfigUpdate +# Takes an original and modified config, and produces the config update tx +# which transitions between the two +# NOTE: this must be run in a CLI container since it requires configtxlator +createConfigUpdate() { + CHANNEL=$1 + ORIGINAL=$2 + MODIFIED=$3 + OUTPUT=$4 + + set -x + configtxlator proto_encode --input "${ORIGINAL}" --type common.Config >original_config.pb + configtxlator proto_encode --input "${MODIFIED}" --type common.Config >modified_config.pb + configtxlator compute_update --channel_id "${CHANNEL}" --original original_config.pb --updated modified_config.pb >config_update.pb + configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate >config_update.json + echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CHANNEL'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . >config_update_in_envelope.json + configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope >"${OUTPUT}" + { set +x; } 2>/dev/null +} + +# signConfigtxAsPeerOrg +# Set the peerOrg admin of an org and sign the config update +signConfigtxAsPeerOrg() { + ORG=$1 + CONFIGTXFILE=$2 + setGlobals $ORG + set -x + peer channel signconfigtx -f "${CONFIGTXFILE}" + { set +x; } 2>/dev/null +} diff --git a/test-network/scripts/createChannel.sh b/test-network/scripts/createChannel.sh new file mode 100755 index 0000000..567155c --- /dev/null +++ b/test-network/scripts/createChannel.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# imports +. scripts/envVar.sh +. scripts/utils.sh + +CHANNEL_NAME="$1" +DELAY="$2" +MAX_RETRY="$3" +VERBOSE="$4" +: ${CHANNEL_NAME:="mychannel"} +: ${DELAY:="3"} +: ${MAX_RETRY:="5"} +: ${VERBOSE:="false"} + +if [ ! -d "channel-artifacts" ]; then + mkdir channel-artifacts +fi + +createChannelTx() { + set -x + configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/${CHANNEL_NAME}.tx -channelID $CHANNEL_NAME + res=$? + { set +x; } 2>/dev/null + verifyResult $res "Failed to generate channel configuration transaction..." +} + +createChannel() { + setGlobals 1 + # Poll in case the raft leader is not set yet + local rc=1 + local COUNTER=1 + while [ $rc -ne 0 -a $COUNTER -lt $MAX_RETRY ] ; do + sleep $DELAY + set -x + peer channel create -o localhost:7050 -c $CHANNEL_NAME --ordererTLSHostnameOverride orderer.example.com -f ./channel-artifacts/${CHANNEL_NAME}.tx --outputBlock $BLOCKFILE --tls --cafile $ORDERER_CA >&log.txt + res=$? + { set +x; } 2>/dev/null + let rc=$res + COUNTER=$(expr $COUNTER + 1) + done + cat log.txt + verifyResult $res "Channel creation failed" +} + +# joinChannel ORG +joinChannel() { + FABRIC_CFG_PATH=$PWD/../config/ + ORG=$1 + setGlobals $ORG + local rc=1 + local COUNTER=1 + ## Sometimes Join takes time, hence retry + while [ $rc -ne 0 -a $COUNTER -lt $MAX_RETRY ] ; do + sleep $DELAY + set -x + peer channel join -b $BLOCKFILE >&log.txt + res=$? + { set +x; } 2>/dev/null + let rc=$res + COUNTER=$(expr $COUNTER + 1) + done + cat log.txt + verifyResult $res "After $MAX_RETRY attempts, peer0.org${ORG} has failed to join channel '$CHANNEL_NAME' " +} + +setAnchorPeer() { + ORG=$1 + docker exec cli ./scripts/setAnchorPeer.sh $ORG $CHANNEL_NAME +} + +FABRIC_CFG_PATH=${PWD}/configtx + +## Create channeltx +infoln "Generating channel create transaction '${CHANNEL_NAME}.tx'" +createChannelTx + +FABRIC_CFG_PATH=$PWD/../config/ +BLOCKFILE="./channel-artifacts/${CHANNEL_NAME}.block" + +## Create channel +infoln "Creating channel ${CHANNEL_NAME}" +createChannel +successln "Channel '$CHANNEL_NAME' created" + +## Join all the peers to the channel +infoln "Joining org1 peer to the channel..." +joinChannel 1 +infoln "Joining org2 peer to the channel..." +joinChannel 2 + +## Set the anchor peers for each org in the channel +infoln "Setting anchor peer for org1..." +setAnchorPeer 1 +infoln "Setting anchor peer for org2..." +setAnchorPeer 2 + +successln "Channel '$CHANNEL_NAME' joined" diff --git a/test-network/scripts/deployCC.sh b/test-network/scripts/deployCC.sh new file mode 100755 index 0000000..fb5f95b --- /dev/null +++ b/test-network/scripts/deployCC.sh @@ -0,0 +1,328 @@ +#!/bin/bash + +source scripts/utils.sh + +CHANNEL_NAME=${1:-"mychannel"} +CC_NAME=${2} +CC_SRC_PATH=${3} +CC_SRC_LANGUAGE=${4} +CC_VERSION=${5:-"1.0"} +CC_SEQUENCE=${6:-"1"} +CC_INIT_FCN=${7:-"NA"} +CC_END_POLICY=${8:-"NA"} +CC_COLL_CONFIG=${9:-"NA"} +DELAY=${10:-"3"} +MAX_RETRY=${11:-"5"} +VERBOSE=${12:-"false"} + +println "executing with the following" +println "- CHANNEL_NAME: ${C_GREEN}${CHANNEL_NAME}${C_RESET}" +println "- CC_NAME: ${C_GREEN}${CC_NAME}${C_RESET}" +println "- CC_SRC_PATH: ${C_GREEN}${CC_SRC_PATH}${C_RESET}" +println "- CC_SRC_LANGUAGE: ${C_GREEN}${CC_SRC_LANGUAGE}${C_RESET}" +println "- CC_VERSION: ${C_GREEN}${CC_VERSION}${C_RESET}" +println "- CC_SEQUENCE: ${C_GREEN}${CC_SEQUENCE}${C_RESET}" +println "- CC_END_POLICY: ${C_GREEN}${CC_END_POLICY}${C_RESET}" +println "- CC_COLL_CONFIG: ${C_GREEN}${CC_COLL_CONFIG}${C_RESET}" +println "- CC_INIT_FCN: ${C_GREEN}${CC_INIT_FCN}${C_RESET}" +println "- DELAY: ${C_GREEN}${DELAY}${C_RESET}" +println "- MAX_RETRY: ${C_GREEN}${MAX_RETRY}${C_RESET}" +println "- VERBOSE: ${C_GREEN}${VERBOSE}${C_RESET}" + +FABRIC_CFG_PATH=$PWD/../config/ + +#User has not provided a name +if [ -z "$CC_NAME" ] || [ "$CC_NAME" = "NA" ]; then + fatalln "No chaincode name was provided. Valid call example: ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go" + +# User has not provided a path +elif [ -z "$CC_SRC_PATH" ] || [ "$CC_SRC_PATH" = "NA" ]; then + fatalln "No chaincode path was provided. Valid call example: ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go" + +# User has not provided a language +elif [ -z "$CC_SRC_LANGUAGE" ] || [ "$CC_SRC_LANGUAGE" = "NA" ]; then + fatalln "No chaincode language was provided. Valid call example: ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go" + +## Make sure that the path to the chaincode exists +elif [ ! -d "$CC_SRC_PATH" ]; then + fatalln "Path to chaincode does not exist. Please provide different path." +fi + +CC_SRC_LANGUAGE=$(echo "$CC_SRC_LANGUAGE" | tr [:upper:] [:lower:]) + +# do some language specific preparation to the chaincode before packaging +if [ "$CC_SRC_LANGUAGE" = "go" ]; then + CC_RUNTIME_LANGUAGE=golang + + infoln "Vendoring Go dependencies at $CC_SRC_PATH" + pushd $CC_SRC_PATH + GO111MODULE=on go mod vendor + popd + successln "Finished vendoring Go dependencies" + +elif [ "$CC_SRC_LANGUAGE" = "java" ]; then + CC_RUNTIME_LANGUAGE=java + + infoln "Compiling Java code..." + pushd $CC_SRC_PATH + ./gradlew installDist + popd + successln "Finished compiling Java code" + CC_SRC_PATH=$CC_SRC_PATH/build/install/$CC_NAME + +elif [ "$CC_SRC_LANGUAGE" = "javascript" ]; then + CC_RUNTIME_LANGUAGE=node + +elif [ "$CC_SRC_LANGUAGE" = "typescript" ]; then + CC_RUNTIME_LANGUAGE=node + + infoln "Compiling TypeScript code into JavaScript..." + pushd $CC_SRC_PATH + npm install + npm run build + popd + successln "Finished compiling TypeScript code into JavaScript" + +else + fatalln "The chaincode language ${CC_SRC_LANGUAGE} is not supported by this script. Supported chaincode languages are: go, java, javascript, and typescript" + exit 1 +fi + +INIT_REQUIRED="--init-required" +# check if the init fcn should be called +if [ "$CC_INIT_FCN" = "NA" ]; then + INIT_REQUIRED="" +fi + +if [ "$CC_END_POLICY" = "NA" ]; then + CC_END_POLICY="" +else + CC_END_POLICY="--signature-policy $CC_END_POLICY" +fi + +if [ "$CC_COLL_CONFIG" = "NA" ]; then + CC_COLL_CONFIG="" +else + CC_COLL_CONFIG="--collections-config $CC_COLL_CONFIG" +fi + +# import utils +. scripts/envVar.sh + +packageChaincode() { + set -x + peer lifecycle chaincode package ${CC_NAME}.tar.gz --path ${CC_SRC_PATH} --lang ${CC_RUNTIME_LANGUAGE} --label ${CC_NAME}_${CC_VERSION} >&log.txt + res=$? + { set +x; } 2>/dev/null + cat log.txt + verifyResult $res "Chaincode packaging has failed" + successln "Chaincode is packaged" +} + +# installChaincode PEER ORG +installChaincode() { + ORG=$1 + setGlobals $ORG + set -x + peer lifecycle chaincode install ${CC_NAME}.tar.gz >&log.txt + res=$? + { set +x; } 2>/dev/null + cat log.txt + verifyResult $res "Chaincode installation on peer0.org${ORG} has failed" + successln "Chaincode is installed on peer0.org${ORG}" +} + +# queryInstalled PEER ORG +queryInstalled() { + ORG=$1 + setGlobals $ORG + set -x + peer lifecycle chaincode queryinstalled >&log.txt + res=$? + { set +x; } 2>/dev/null + cat log.txt + PACKAGE_ID=$(sed -n "/${CC_NAME}_${CC_VERSION}/{s/^Package ID: //; s/, Label:.*$//; p;}" log.txt) + verifyResult $res "Query installed on peer0.org${ORG} has failed" + successln "Query installed successful on peer0.org${ORG} on channel" +} + +# approveForMyOrg VERSION PEER ORG +approveForMyOrg() { + ORG=$1 + setGlobals $ORG + set -x + peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile $ORDERER_CA --channelID $CHANNEL_NAME --name ${CC_NAME} --version ${CC_VERSION} --package-id ${PACKAGE_ID} --sequence ${CC_SEQUENCE} ${INIT_REQUIRED} ${CC_END_POLICY} ${CC_COLL_CONFIG} >&log.txt + res=$? + { set +x; } 2>/dev/null + cat log.txt + verifyResult $res "Chaincode definition approved on peer0.org${ORG} on channel '$CHANNEL_NAME' failed" + successln "Chaincode definition approved on peer0.org${ORG} on channel '$CHANNEL_NAME'" +} + +# checkCommitReadiness VERSION PEER ORG +checkCommitReadiness() { + ORG=$1 + shift 1 + setGlobals $ORG + infoln "Checking the commit readiness of the chaincode definition on peer0.org${ORG} on channel '$CHANNEL_NAME'..." + local rc=1 + local COUNTER=1 + # continue to poll + # we either get a successful response, or reach MAX RETRY + while [ $rc -ne 0 -a $COUNTER -lt $MAX_RETRY ]; do + sleep $DELAY + infoln "Attempting to check the commit readiness of the chaincode definition on peer0.org${ORG}, Retry after $DELAY seconds." + set -x + peer lifecycle chaincode checkcommitreadiness --channelID $CHANNEL_NAME --name ${CC_NAME} --version ${CC_VERSION} --sequence ${CC_SEQUENCE} ${INIT_REQUIRED} ${CC_END_POLICY} ${CC_COLL_CONFIG} --output json >&log.txt + res=$? + { set +x; } 2>/dev/null + let rc=0 + for var in "$@"; do + grep "$var" log.txt &>/dev/null || let rc=1 + done + COUNTER=$(expr $COUNTER + 1) + done + cat log.txt + if test $rc -eq 0; then + infoln "Checking the commit readiness of the chaincode definition successful on peer0.org${ORG} on channel '$CHANNEL_NAME'" + else + fatalln "After $MAX_RETRY attempts, Check commit readiness result on peer0.org${ORG} is INVALID!" + fi +} + +# commitChaincodeDefinition VERSION PEER ORG (PEER ORG)... +commitChaincodeDefinition() { + parsePeerConnectionParameters $@ + res=$? + verifyResult $res "Invoke transaction failed on channel '$CHANNEL_NAME' due to uneven number of peer and org parameters " + + # while 'peer chaincode' command can get the orderer endpoint from the + # peer (if join was successful), let's supply it directly as we know + # it using the "-o" option + set -x + peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile $ORDERER_CA --channelID $CHANNEL_NAME --name ${CC_NAME} $PEER_CONN_PARMS --version ${CC_VERSION} --sequence ${CC_SEQUENCE} ${INIT_REQUIRED} ${CC_END_POLICY} ${CC_COLL_CONFIG} >&log.txt + res=$? + { set +x; } 2>/dev/null + cat log.txt + verifyResult $res "Chaincode definition commit failed on peer0.org${ORG} on channel '$CHANNEL_NAME' failed" + successln "Chaincode definition committed on channel '$CHANNEL_NAME'" +} + +# queryCommitted ORG +queryCommitted() { + ORG=$1 + setGlobals $ORG + EXPECTED_RESULT="Version: ${CC_VERSION}, Sequence: ${CC_SEQUENCE}, Endorsement Plugin: escc, Validation Plugin: vscc" + infoln "Querying chaincode definition on peer0.org${ORG} on channel '$CHANNEL_NAME'..." + local rc=1 + local COUNTER=1 + # continue to poll + # we either get a successful response, or reach MAX RETRY + while [ $rc -ne 0 -a $COUNTER -lt $MAX_RETRY ]; do + sleep $DELAY + infoln "Attempting to Query committed status on peer0.org${ORG}, Retry after $DELAY seconds." + set -x + peer lifecycle chaincode querycommitted --channelID $CHANNEL_NAME --name ${CC_NAME} >&log.txt + res=$? + { set +x; } 2>/dev/null + test $res -eq 0 && VALUE=$(cat log.txt | grep -o '^Version: '$CC_VERSION', Sequence: [0-9]*, Endorsement Plugin: escc, Validation Plugin: vscc') + test "$VALUE" = "$EXPECTED_RESULT" && let rc=0 + COUNTER=$(expr $COUNTER + 1) + done + cat log.txt + if test $rc -eq 0; then + successln "Query chaincode definition successful on peer0.org${ORG} on channel '$CHANNEL_NAME'" + else + fatalln "After $MAX_RETRY attempts, Query chaincode definition result on peer0.org${ORG} is INVALID!" + fi +} + +chaincodeInvokeInit() { + parsePeerConnectionParameters $@ + res=$? + verifyResult $res "Invoke transaction failed on channel '$CHANNEL_NAME' due to uneven number of peer and org parameters " + + # while 'peer chaincode' command can get the orderer endpoint from the + # peer (if join was successful), let's supply it directly as we know + # it using the "-o" option + set -x + fcn_call='{"function":"'${CC_INIT_FCN}'","Args":[]}' + infoln "invoke fcn call:${fcn_call}" + peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile $ORDERER_CA -C $CHANNEL_NAME -n ${CC_NAME} $PEER_CONN_PARMS --isInit -c ${fcn_call} >&log.txt + res=$? + { set +x; } 2>/dev/null + cat log.txt + verifyResult $res "Invoke execution on $PEERS failed " + successln "Invoke transaction successful on $PEERS on channel '$CHANNEL_NAME'" +} + +chaincodeQuery() { + ORG=$1 + setGlobals $ORG + infoln "Querying on peer0.org${ORG} on channel '$CHANNEL_NAME'..." + local rc=1 + local COUNTER=1 + # continue to poll + # we either get a successful response, or reach MAX RETRY + while [ $rc -ne 0 -a $COUNTER -lt $MAX_RETRY ]; do + sleep $DELAY + infoln "Attempting to Query peer0.org${ORG}, Retry after $DELAY seconds." + set -x + peer chaincode query -C $CHANNEL_NAME -n ${CC_NAME} -c '{"Args":["queryAllCars"]}' >&log.txt + res=$? + { set +x; } 2>/dev/null + let rc=$res + COUNTER=$(expr $COUNTER + 1) + done + cat log.txt + if test $rc -eq 0; then + successln "Query successful on peer0.org${ORG} on channel '$CHANNEL_NAME'" + else + fatalln "After $MAX_RETRY attempts, Query result on peer0.org${ORG} is INVALID!" + fi +} + +## package the chaincode +packageChaincode + +## Install chaincode on peer0.org1 and peer0.org2 +infoln "Installing chaincode on peer0.org1..." +installChaincode 1 +infoln "Install chaincode on peer0.org2..." +installChaincode 2 + +## query whether the chaincode is installed +queryInstalled 1 + +## approve the definition for org1 +approveForMyOrg 1 + +## check whether the chaincode definition is ready to be committed +## expect org1 to have approved and org2 not to +checkCommitReadiness 1 "\"Org1MSP\": true" "\"Org2MSP\": false" +checkCommitReadiness 2 "\"Org1MSP\": true" "\"Org2MSP\": false" + +## now approve also for org2 +approveForMyOrg 2 + +## check whether the chaincode definition is ready to be committed +## expect them both to have approved +checkCommitReadiness 1 "\"Org1MSP\": true" "\"Org2MSP\": true" +checkCommitReadiness 2 "\"Org1MSP\": true" "\"Org2MSP\": true" + +## now that we know for sure both orgs have approved, commit the definition +commitChaincodeDefinition 1 2 + +## query on both orgs to see that the definition committed successfully +queryCommitted 1 +queryCommitted 2 + +## Invoke the chaincode - this does require that the chaincode have the 'initLedger' +## method defined +if [ "$CC_INIT_FCN" = "NA" ]; then + infoln "Chaincode initialization is not required" +else + chaincodeInvokeInit 1 2 +fi + +exit 0 diff --git a/test-network/scripts/envVar.sh b/test-network/scripts/envVar.sh new file mode 100755 index 0000000..4ac40c0 --- /dev/null +++ b/test-network/scripts/envVar.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +# This is a collection of bash functions used by different scripts + +# imports +. scripts/utils.sh + +export CORE_PEER_TLS_ENABLED=true +export ORDERER_CA=${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem +export PEER0_ORG1_CA=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export PEER0_ORG2_CA=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +export PEER0_ORG3_CA=${PWD}/organizations/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt + +# Set environment variables for the peer org +setGlobals() { + local USING_ORG="" + if [ -z "$OVERRIDE_ORG" ]; then + USING_ORG=$1 + else + USING_ORG="${OVERRIDE_ORG}" + fi + infoln "Using organization ${USING_ORG}" + if [ $USING_ORG -eq 1 ]; then + export CORE_PEER_LOCALMSPID="Org1MSP" + export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG1_CA + export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp + export CORE_PEER_ADDRESS=localhost:7051 + elif [ $USING_ORG -eq 2 ]; then + export CORE_PEER_LOCALMSPID="Org2MSP" + export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG2_CA + export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp + export CORE_PEER_ADDRESS=localhost:9051 + + elif [ $USING_ORG -eq 3 ]; then + export CORE_PEER_LOCALMSPID="Org3MSP" + export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG3_CA + export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp + export CORE_PEER_ADDRESS=localhost:11051 + else + errorln "ORG Unknown" + fi + + if [ "$VERBOSE" == "true" ]; then + env | grep CORE + fi +} + +# Set environment variables for use in the CLI container +setGlobalsCLI() { + setGlobals $1 + + local USING_ORG="" + if [ -z "$OVERRIDE_ORG" ]; then + USING_ORG=$1 + else + USING_ORG="${OVERRIDE_ORG}" + fi + if [ $USING_ORG -eq 1 ]; then + export CORE_PEER_ADDRESS=peer0.org1.example.com:7051 + elif [ $USING_ORG -eq 2 ]; then + export CORE_PEER_ADDRESS=peer0.org2.example.com:9051 + elif [ $USING_ORG -eq 3 ]; then + export CORE_PEER_ADDRESS=peer0.org3.example.com:11051 + else + errorln "ORG Unknown" + fi +} + +# parsePeerConnectionParameters $@ +# Helper function that sets the peer connection parameters for a chaincode +# operation +parsePeerConnectionParameters() { + PEER_CONN_PARMS="" + PEERS="" + while [ "$#" -gt 0 ]; do + setGlobals $1 + PEER="peer0.org$1" + ## Set peer addresses + PEERS="$PEERS $PEER" + PEER_CONN_PARMS="$PEER_CONN_PARMS --peerAddresses $CORE_PEER_ADDRESS" + ## Set path to TLS certificate + TLSINFO=$(eval echo "--tlsRootCertFiles \$PEER0_ORG$1_CA") + PEER_CONN_PARMS="$PEER_CONN_PARMS $TLSINFO" + # shift by one to get to the next organization + shift + done + # remove leading space for output + PEERS="$(echo -e "$PEERS" | sed -e 's/^[[:space:]]*//')" +} + +verifyResult() { + if [ $1 -ne 0 ]; then + fatalln "$2" + fi +} diff --git a/test-network/scripts/org3-scripts/joinChannel.sh b/test-network/scripts/org3-scripts/joinChannel.sh new file mode 100755 index 0000000..97153cc --- /dev/null +++ b/test-network/scripts/org3-scripts/joinChannel.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# This script is designed to be run in the cli container as the +# second step of the EYFN tutorial. It joins the org3 peers to the +# channel previously setup in the BYFN tutorial and install the +# chaincode as version 2.0 on peer0.org3. +# + +CHANNEL_NAME="$1" +DELAY="$2" +TIMEOUT="$3" +VERBOSE="$4" +: ${CHANNEL_NAME:="mychannel"} +: ${DELAY:="3"} +: ${TIMEOUT:="10"} +: ${VERBOSE:="false"} +COUNTER=1 +MAX_RETRY=5 + +# import environment variables +. scripts/envVar.sh + +# joinChannel ORG +joinChannel() { + ORG=$1 + local rc=1 + local COUNTER=1 + ## Sometimes Join takes time, hence retry + while [ $rc -ne 0 -a $COUNTER -lt $MAX_RETRY ] ; do + sleep $DELAY + set -x + peer channel join -b $BLOCKFILE >&log.txt + res=$? + { set +x; } 2>/dev/null + let rc=$res + COUNTER=$(expr $COUNTER + 1) + done + cat log.txt + verifyResult $res "After $MAX_RETRY attempts, peer0.org${ORG} has failed to join channel '$CHANNEL_NAME' " +} + +setAnchorPeer() { + ORG=$1 + scripts/setAnchorPeer.sh $ORG $CHANNEL_NAME +} + +setGlobalsCLI 3 +BLOCKFILE="${CHANNEL_NAME}.block" + +echo "Fetching channel config block from orderer..." +set -x +peer channel fetch 0 $BLOCKFILE -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com -c $CHANNEL_NAME --tls --cafile $ORDERER_CA >&log.txt +res=$? +{ set +x; } 2>/dev/null +cat log.txt +verifyResult $res "Fetching config block from orderer has failed" + +infoln "Joining org3 peer to the channel..." +joinChannel 3 + +infoln "Setting anchor peer for org3..." +setAnchorPeer 3 + +successln "Channel '$CHANNEL_NAME' joined" +successln "Org3 peer successfully added to network" diff --git a/test-network/scripts/org3-scripts/updateChannelConfig.sh b/test-network/scripts/org3-scripts/updateChannelConfig.sh new file mode 100755 index 0000000..eabbf71 --- /dev/null +++ b/test-network/scripts/org3-scripts/updateChannelConfig.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# This script is designed to be run in the cli container as the +# first step of the EYFN tutorial. It creates and submits a +# configuration transaction to add org3 to the test network +# + +CHANNEL_NAME="$1" +DELAY="$2" +TIMEOUT="$3" +VERBOSE="$4" +: ${CHANNEL_NAME:="mychannel"} +: ${DELAY:="3"} +: ${TIMEOUT:="10"} +: ${VERBOSE:="false"} +COUNTER=1 +MAX_RETRY=5 + + +# imports +. scripts/envVar.sh +. scripts/configUpdate.sh +. scripts/utils.sh + +infoln "Creating config transaction to add org3 to network" + +# Fetch the config for the channel, writing it to config.json +fetchChannelConfig 1 ${CHANNEL_NAME} config.json + +# Modify the configuration to append the new org +set -x +jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"Org3MSP":.[1]}}}}}' config.json ./organizations/peerOrganizations/org3.example.com/org3.json > modified_config.json +{ set +x; } 2>/dev/null + +# Compute a config update, based on the differences between config.json and modified_config.json, write it as a transaction to org3_update_in_envelope.pb +createConfigUpdate ${CHANNEL_NAME} config.json modified_config.json org3_update_in_envelope.pb + +infoln "Signing config transaction" +signConfigtxAsPeerOrg 1 org3_update_in_envelope.pb + +infoln "Submitting transaction from a different peer (peer0.org2) which also signs it" +setGlobals 2 +set -x +peer channel update -f org3_update_in_envelope.pb -c ${CHANNEL_NAME} -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${ORDERER_CA} +{ set +x; } 2>/dev/null + +successln "Config transaction to add org3 to network submitted" diff --git a/test-network/scripts/setAnchorPeer.sh b/test-network/scripts/setAnchorPeer.sh new file mode 100755 index 0000000..d0ac00d --- /dev/null +++ b/test-network/scripts/setAnchorPeer.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# import utils +. scripts/envVar.sh +. scripts/configUpdate.sh + + +# NOTE: this must be run in a CLI container since it requires jq and configtxlator +createAnchorPeerUpdate() { + infoln "Fetching channel config for channel $CHANNEL_NAME" + fetchChannelConfig $ORG $CHANNEL_NAME ${CORE_PEER_LOCALMSPID}config.json + + infoln "Generating anchor peer update transaction for Org${ORG} on channel $CHANNEL_NAME" + + if [ $ORG -eq 1 ]; then + HOST="peer0.org1.example.com" + PORT=7051 + elif [ $ORG -eq 2 ]; then + HOST="peer0.org2.example.com" + PORT=9051 + elif [ $ORG -eq 3 ]; then + HOST="peer0.org3.example.com" + PORT=11051 + else + errorln "Org${ORG} unknown" + fi + + set -x + # Modify the configuration to append the anchor peer + jq '.channel_group.groups.Application.groups.'${CORE_PEER_LOCALMSPID}'.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "'$HOST'","port": '$PORT'}]},"version": "0"}}' ${CORE_PEER_LOCALMSPID}config.json > ${CORE_PEER_LOCALMSPID}modified_config.json + { set +x; } 2>/dev/null + + # Compute a config update, based on the differences between + # {orgmsp}config.json and {orgmsp}modified_config.json, write + # it as a transaction to {orgmsp}anchors.tx + createConfigUpdate ${CHANNEL_NAME} ${CORE_PEER_LOCALMSPID}config.json ${CORE_PEER_LOCALMSPID}modified_config.json ${CORE_PEER_LOCALMSPID}anchors.tx +} + +updateAnchorPeer() { + peer channel update -o orderer.example.com:7050 --ordererTLSHostnameOverride orderer.example.com -c $CHANNEL_NAME -f ${CORE_PEER_LOCALMSPID}anchors.tx --tls --cafile $ORDERER_CA >&log.txt + res=$? + cat log.txt + verifyResult $res "Anchor peer update failed" + successln "Anchor peer set for org '$CORE_PEER_LOCALMSPID' on channel '$CHANNEL_NAME'" +} + +ORG=$1 +CHANNEL_NAME=$2 +setGlobalsCLI $ORG + +createAnchorPeerUpdate + +updateAnchorPeer diff --git a/test-network/scripts/utils.sh b/test-network/scripts/utils.sh new file mode 100755 index 0000000..125cfb7 --- /dev/null +++ b/test-network/scripts/utils.sh @@ -0,0 +1,156 @@ +#!/bin/bash + +C_RESET='\033[0m' +C_RED='\033[0;31m' +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." + println " -ccl - Programming language of chaincode to deploy: go, 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 - File path to the chaincode." + 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 -ccp ../asset-transfer-basic/chaincode-javascript/ ./ -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." + println " -ccl - Programming language of the chaincode to deploy: go, 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 - File path to the chaincode." + 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 -ccp ../asset-transfer-basic/chaincode-javascript/ -ccl javascript" + println " network.sh deployCC -ccn mychaincode -ccp ./user/mychaincode -ccv 1 -ccl javascript" + fi +} + +# println echos string +function println() { + echo -e "$1" +} + +# errorln echos i red color +function errorln() { + println "${C_RED}${1}${C_RESET}" +} + +# successln echos in green color +function successln() { + println "${C_GREEN}${1}${C_RESET}" +} + +# infoln echos in blue color +function infoln() { + println "${C_BLUE}${1}${C_RESET}" +} + +# warnln echos in yellow color +function warnln() { + println "${C_YELLOW}${1}${C_RESET}" +} + +# fatalln echos in red color and exits with fail status +function fatalln() { + errorln "$1" + exit 1 +} + +export -f errorln +export -f successln +export -f infoln +export -f warnln diff --git a/token-erc-20/README.md b/token-erc-20/README.md new file mode 100644 index 0000000..e08773c --- /dev/null +++ b/token-erc-20/README.md @@ -0,0 +1,403 @@ +# ERC-20 token scenario + +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. +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` 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 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 +``` + +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 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_erc20 -ccp ../token-erc-20/chaincode-go/ -ccl go +``` + +**For a JavaScript Contract:** +``` +./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_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. + +## 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 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 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 +``` + +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 PATH=${PWD}/../bin:${PWD}:$PATH +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 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 +``` + +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_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_erc20 -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 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_erc20 -c '{"function":"ClientAccountID","Args":[]}' +``` + +**For a Go Contract:** + +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 +``` + +**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:** +``` +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_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. +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_erc20 -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_erc20 -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. + +## 3rd party transfers (TransferFrom) + +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 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. + +## 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. + +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 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 +``` + +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_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: +``` +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 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_erc20 -c '{"function":"Approve","Args":["'"$SPENDER"'", "500"]}' +``` + +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:** + +``` +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_erc20 -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. + +**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_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 for the spender, let's request the minter's account balance again: +``` +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: +``` +4800 +``` + +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_erc20 -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_erc20 -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: +``` +./network.sh down +``` + +## Contract extension ideas + +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 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. diff --git a/token-erc-20/chaincode-go/chaincode/token_contract.go b/token-erc-20/chaincode-go/chaincode/token_contract.go new file mode 100644 index 0000000..66d2411 --- /dev/null +++ b/token-erc-20/chaincode-go/chaincode/token_contract.go @@ -0,0 +1,479 @@ +package chaincode + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "strconv" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// 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 +} + +// event provides an organized struct for emitting events +type event struct { + from string + to string + value int +} + +// Mint creates new tokens and adds them to minter's account balance +// This function triggers a Transfer event +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 + } + + // 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 + err = ctx.GetStub().PutState(totalSupplyKey, []byte(strconv.Itoa(totalSupply))) + if err != nil { + return err + } + + // Emit the Transfer event + transferEvent := event{"0x0", minter, 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("failed to set event: %v", err) + } + + log.Printf("minter account %s balance updated from %d to %d", minter, currentBalance, updatedBalance) + + return nil +} + +// Burn redeems tokens the minter's account balance +// This function triggers a Transfer event +func (s *SmartContract) Burn(ctx contractapi.TransactionContextInterface, amount int) error { + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to burn 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 errors.New("burn 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 + + // Check if minter current balance exists + if currentBalanceBytes == nil { + return errors.New("The balance does not exist") + } + + 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 + } + + // Update the totalSupply + totalSupplyBytes, err := ctx.GetStub().GetState(totalSupplyKey) + if err != nil { + return fmt.Errorf("failed to retrieve total token supply: %v", err) + } + + // If no tokens have been minted, throw error + if totalSupplyBytes == nil { + return errors.New("totalSupply does not exist") + } + + totalSupply, _ := strconv.Atoi(string(totalSupplyBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer. + + // Subtract the burn amount to the total supply and update the state + totalSupply -= amount + err = ctx.GetStub().PutState(totalSupplyKey, []byte(strconv.Itoa(totalSupply))) + if err != nil { + return err + } + + // Emit the Transfer event + transferEvent := event{minter, "0x0", 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("failed to set event: %v", 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 +// This function triggers a Transfer event +func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, recipient string, amount int) 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) + } + + err = transferHelper(ctx, clientID, recipient, amount) + if err != nil { + return fmt.Errorf("failed to transfer: %v", err) + } + + // Emit the Transfer event + transferEvent := event{clientID, recipient, 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("failed to set event: %v", err) + } + + 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 +} + +// 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("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 +// This function triggers an Approval event +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) + } + + // Emit the Approval event + approvalEvent := event{owner, spender, 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("failed to set event: %v", 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 + } + + // Emit the Transfer event + transferEvent := event{from, to, value} + 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("failed to set event: %v", 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 ERC-20, 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 +} diff --git a/token-erc-20/chaincode-go/go.mod b/token-erc-20/chaincode-go/go.mod new file mode 100644 index 0000000..6bee34a --- /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-erc-20/chaincode-go/go.sum b/token-erc-20/chaincode-go/go.sum new file mode 100644 index 0000000..5a92905 --- /dev/null +++ b/token-erc-20/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-erc-20/chaincode-go/token_erc_20.go b/token-erc-20/chaincode-go/token_erc_20.go new file mode 100644 index 0000000..2bcc51d --- /dev/null +++ b/token-erc-20/chaincode-go/token_erc_20.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-erc-20/chaincode-go/chaincode" +) + +func main() { + tokenChaincode, err := contractapi.NewChaincode(&chaincode.SmartContract{}) + if err != nil { + log.Panicf("Error creating token-erc-20 chaincode: %v", err) + } + + if err := tokenChaincode.Start(); err != nil { + log.Panicf("Error starting token-erc-20 chaincode: %v", err) + } +} diff --git a/token-erc-20/chaincode-javascript/.editorconfig b/token-erc-20/chaincode-javascript/.editorconfig new file mode 100755 index 0000000..75a13be --- /dev/null +++ b/token-erc-20/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-erc-20/chaincode-javascript/.eslintignore b/token-erc-20/chaincode-javascript/.eslintignore new file mode 100644 index 0000000..1595847 --- /dev/null +++ b/token-erc-20/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/token-erc-20/chaincode-javascript/.eslintrc.js b/token-erc-20/chaincode-javascript/.eslintrc.js new file mode 100644 index 0000000..8d99762 --- /dev/null +++ b/token-erc-20/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-erc-20/chaincode-javascript/.gitignore b/token-erc-20/chaincode-javascript/.gitignore new file mode 100644 index 0000000..c84ff1d --- /dev/null +++ b/token-erc-20/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-erc-20/chaincode-javascript/index.js b/token-erc-20/chaincode-javascript/index.js new file mode 100644 index 0000000..1841d31 --- /dev/null +++ b/token-erc-20/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-erc-20/chaincode-javascript/lib/tokenERC20.js b/token-erc-20/chaincode-javascript/lib/tokenERC20.js new file mode 100644 index 0000000..7ca6774 --- /dev/null +++ b/token-erc-20/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-erc-20/chaincode-javascript/package.json b/token-erc-20/chaincode-javascript/package.json new file mode 100644 index 0000000..e704f98 --- /dev/null +++ b/token-erc-20/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-erc-20/chaincode-javascript/test/tokenERC20.test.js b/token-erc-20/chaincode-javascript/test/tokenERC20.test.js new file mode 100644 index 0000000..0f02b77 --- /dev/null +++ b/token-erc-20/chaincode-javascript/test/tokenERC20.test.js @@ -0,0 +1,281 @@ +/* +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('#SetOption', () => { + it('should work', async () => { + const response = await token.SetOption(ctx, 'some name', 'some symbol', '2'); + sinon.assert.calledWith(mockStub.putState, 'name', Buffer.from('some name')); + sinon.assert.calledWith(mockStub.putState, 'symbol', Buffer.from('some symbol')); + sinon.assert.calledWith(mockStub.putState, 'decimals', Buffer.from('2')); + expect(response).to.equals(true); + }); + }); + + 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}'); + }); + }); + +}); diff --git a/token-utxo/README.md b/token-utxo/README.md new file mode 100644 index 0000000..7d26fc0 --- /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/ -ccl 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 PATH=${PWD}/../bin:${PWD}:$PATH +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 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 0000000..963e11a --- /dev/null +++ b/token-utxo/chaincode-go/chaincode/token_contract.go @@ -0,0 +1,221 @@ +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 + 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) + } + + // 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[utxoInputKey] = 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 0000000..9cdc693 --- /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 0000000..5a92905 --- /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 0000000..481a6d3 --- /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) + } +}