diff --git a/asset-transfer-basic/rest-api-typescript/README.md b/asset-transfer-basic/rest-api-typescript/README.md index ab928154..f5e0953a 100644 --- a/asset-transfer-basic/rest-api-typescript/README.md +++ b/asset-transfer-basic/rest-api-typescript/README.md @@ -58,7 +58,7 @@ The `202 ACCEPTED` response includes a `jobId` which can be used with the `/api/ Jobs are not used to get assets, because evaluating transactions is typically much faster. Related files: -- [src/asset.router.ts](src/asset.router.ts) +- [src/assets.router.ts](src/assets.router.ts) Defines the main `/api/assets` endpoint. - [src/fabric.ts](src/fabric.ts) All the sample code which interacts with the Fabric network via the Fabric SDK. @@ -66,7 +66,7 @@ Related files: Defines the `/api/jobs` endpoint for getting job status. - [src/jobs.ts](src/jobs.ts) Job queue implementation details. -- [src/transactions.router.ts]() +- [src/transactions.router.ts](src/transactions.router.ts) Defines the `/api/transactions` endpoint for getting transaction status. **Note:** If you are not specifically interested in REST APIs, you should only need to look at the files in the [Fabric network connections](#fabric-network-connections) and [Error handling](#error-handling) sections above. diff --git a/asset-transfer-basic/rest-api-typescript/package-lock.json b/asset-transfer-basic/rest-api-typescript/package-lock.json index c1650fea..6f6aa39a 100644 --- a/asset-transfer-basic/rest-api-typescript/package-lock.json +++ b/asset-transfer-basic/rest-api-typescript/package-lock.json @@ -5570,9 +5570,9 @@ "dev": true }, "node_modules/moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "version": "2.29.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", "engines": { "node": "*" } @@ -11993,9 +11993,9 @@ "dev": true }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "version": "2.29.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" }, "moment-timezone": { "version": "0.5.33", @@ -13570,4 +13570,4 @@ "integrity": "sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==" } } -} \ No newline at end of file +} diff --git a/test-network-k8s/docs/CA.md b/test-network-k8s/docs/CA.md index 48624536..f10302ef 100644 --- a/test-network-k8s/docs/CA.md +++ b/test-network-k8s/docs/CA.md @@ -72,7 +72,7 @@ simplified, but realistic CA deployment illustrating key touch points with Kuber ### Future Enhancements: -- **_Bring your own Certificates_** : It would be nice to boostrap the network using a single, top-level signing authority, +- **_Bring your own Certificates_** : It would be nice to bootstrap the network using a single, top-level signing authority, rather than generating self-signed certificates when the system is bootstrapped. Ideally this will be realized by introducing an [Intermediate CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/ca-deploy-topology.html#when-would-i-want-an-intermediate-ca) and/or alternate signing chains backed by formal (e.g. letsencrypt, Thawte, Verisign, etc.) certificate authorities. diff --git a/test-network-k8s/docs/CHANNELS.md b/test-network-k8s/docs/CHANNELS.md index b72e5d92..cfc23980 100644 --- a/test-network-k8s/docs/CHANNELS.md +++ b/test-network-k8s/docs/CHANNELS.md @@ -33,7 +33,7 @@ In order to construct a Fabric channel, the following steps must be performed: 4. Network orderers are joined to the channel using the channel participation API. -5. Network peers are joioned to the channel. +5. Network peers are joined to the channel. ## Aggregating the Channel MSP diff --git a/test-network-k8s/docs/HIGH_AVAILABILITY.md b/test-network-k8s/docs/HIGH_AVAILABILITY.md index 585f09cc..1725e4bc 100644 --- a/test-network-k8s/docs/HIGH_AVAILABILITY.md +++ b/test-network-k8s/docs/HIGH_AVAILABILITY.md @@ -4,7 +4,7 @@ The peers have been configured so they implemented a essential failover/high-ava Two important notes: -1. The word 'gateway' in the k8s definitions is being used in a generic way. It is not tied to the concept of the 'Fabric Gateway' component. However using the 'Fabric-Gateway' with the udpated SDKs, make connecting to Fabric even easier. There is a single connection, that can easily be handled with core k8s abilities. Attempting the approach described below with the older SDKs is not recommended. +1. The word 'gateway' in the k8s definitions is being used in a generic way. It is not tied to the concept of the 'Fabric Gateway' component. However using the 'Fabric-Gateway' with the updated SDKs, make connecting to Fabric even easier. There is a single connection, that can easily be handled with core k8s abilities. Attempting the approach described below with the older SDKs is not recommended. 2. Long Lived gRPC connections. Remember that the connections between components in Fabric are long-lived gRPC connections. From a client application's perspective that means the connection will be load-balanced when initially connected, but unless the connection breaks, it will not be 're-load-balanced'. It's important to keep this in mind. ## Peer Gateway Services @@ -39,7 +39,7 @@ The selector is `org: org2` that is defined in the specification of the Peer's D ``` ## Kube Proxy Configuration -The proxy configuration is set to be `ipvs`. This gives a lot more scope for different load balancing algorthms. +The proxy configuration is set to be `ipvs`. This gives a lot more scope for different load balancing algorithms. "Round Robin" is the default configuration (as used in this test network). For more information check this [deep dive](https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive) on the Kubernetes blog. For this KIND cluster, this is configured by updating the cluster configuration, add the following yaml. diff --git a/test-network-k8s/kubectl b/test-network-k8s/kubectl new file mode 100644 index 00000000..848c8ed6 Binary files /dev/null and b/test-network-k8s/kubectl differ diff --git a/test-network-k8s/network b/test-network-k8s/network index 955aef67..487e4617 100755 --- a/test-network-k8s/network +++ b/test-network-k8s/network @@ -174,6 +174,11 @@ elif [ "${MODE}" == "chaincode" ]; then elif [ "${MODE}" == "anchor" ]; then update_anchor_peers $@ +elif [ "${MODE}" == "load-images-for-rest-easy" ]; then + log "Loading images for fabric-rest-sample to KIND:" + load_docker_images_for_rest_sample + log "🏁 - Images loaded." + elif [ "${MODE}" == "rest-easy" ]; then log "Launching fabric-rest-sample application:" launch_rest_sample diff --git a/test-network-k8s/scripts/kind.sh b/test-network-k8s/scripts/kind.sh index 14cd6279..88f01b2f 100755 --- a/test-network-k8s/scripts/kind.sh +++ b/test-network-k8s/scripts/kind.sh @@ -13,6 +13,7 @@ function pull_docker_images() { docker pull ${FABRIC_CONTAINER_REGISTRY}/fabric-peer:$FABRIC_VERSION docker pull ${FABRIC_CONTAINER_REGISTRY}/fabric-tools:$FABRIC_VERSION docker pull ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic:latest + docker pull couchdb:3.2.1 pop_fn } @@ -26,10 +27,29 @@ function load_docker_images() { kind load docker-image ${FABRIC_CONTAINER_REGISTRY}/fabric-tools:$FABRIC_VERSION kind load docker-image ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic:latest kind load docker-image couchdb:3.2.1 - - pop_fn + + pop_fn } +function pull_docker_images_for_rest_sample() { + push_fn "Pulling docker images for fabric-rest-sample" + + docker pull ghcr.io/hyperledger/fabric-rest-sample:latest + docker pull redis:6.2.5 + + pop_fn +} + +function load_docker_images_for_rest_sample() { + push_fn "Loading docker images for fabric-rest-sample to KIND control plane" + + kind load docker-image ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic:latest + kind load docker-image redis:6.2.5 + + pop_fn +} + + function apply_nginx_ingress() { push_fn "Launching ingress controller" @@ -181,8 +201,10 @@ function kind_init() { launch_docker_registry if [ "${STAGE_DOCKER_IMAGES}" == true ]; then - pull_docker_images - load_docker_images + pull_docker_images + load_docker_images + pull_docker_images_for_rest_sample + load_docker_images_for_rest_sample fi wait_for_cert_manager diff --git a/test-network/addOrg3/ccp-generate.sh b/test-network/addOrg3/ccp-generate.sh index a3f254f3..a361a9d4 100755 --- a/test-network/addOrg3/ccp-generate.sh +++ b/test-network/addOrg3/ccp-generate.sh @@ -23,7 +23,7 @@ function yaml_ccp { -e "s/\${CAPORT}/$3/" \ -e "s#\${PEERPEM}#$PP#" \ -e "s#\${CAPEM}#$CP#" \ - ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g' + ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g' } ORG=3 diff --git a/test-network/addOrg3/ccp-template.yaml b/test-network/addOrg3/ccp-template.yaml index 7e65965f..b675c186 100644 --- a/test-network/addOrg3/ccp-template.yaml +++ b/test-network/addOrg3/ccp-template.yaml @@ -19,7 +19,7 @@ peers: url: grpcs://localhost:${P0PORT} tlsCACerts: pem: | - ${PEERPEM} + ${PEERPEM} grpcOptions: ssl-target-name-override: peer0.org${ORG}.example.com hostnameOverride: peer0.org${ORG}.example.com @@ -28,7 +28,8 @@ certificateAuthorities: url: https://localhost:${CAPORT} caName: ca-org${ORG} tlsCACerts: - pem: | - ${CAPEM} + pem: + - | + ${CAPEM} httpOptions: verify: false diff --git a/token-erc-20/chaincode-java/Dockerfile b/token-erc-20/chaincode-java/Dockerfile index fbd053c8..55aa22ba 100755 --- a/token-erc-20/chaincode-java/Dockerfile +++ b/token-erc-20/chaincode-java/Dockerfile @@ -1,31 +1,32 @@ -# the first stage -FROM gradle:jdk11 AS GRADLE_BUILD - -# copy the build.gradle and src code to the container -COPY src/ src/ -COPY build.gradle ./ - -# Build and package our code -RUN gradle --no-daemon build shadowJar -x checkstyleMain -x checkstyleTest - - -# the second stage of our build just needs the compiled files -FROM openjdk:11-jre -ARG CC_SERVER_PORT=9999 - -# Setup tini to work better handle signals -ENV TINI_VERSION v0.19.0 -ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini -RUN chmod +x /tini - -RUN addgroup --system javauser && useradd -g javauser javauser - -# copy only the artifacts we need from the first stage and discard the rest -COPY --chown=javauser:javauser --from=GRADLE_BUILD /home/gradle/build/libs/chaincode.jar /chaincode.jar -COPY --chown=javauser:javauser docker/docker-entrypoint.sh /docker-entrypoint.sh -RUN chmod +x /docker-entrypoint.sh -ENV PORT $CC_SERVER_PORT -EXPOSE $CC_SERVER_PORT - -USER javauser -ENTRYPOINT [ "/tini", "--", "/docker-entrypoint.sh" ] +# SPDX-License-Identifier: Apache-2.0 +# the first stage +FROM gradle:jdk11 AS GRADLE_BUILD + +# copy the build.gradle and src code to the container +COPY src/ src/ +COPY build.gradle ./ + +# Build and package our code +RUN gradle --no-daemon build shadowJar -x checkstyleMain -x checkstyleTest + + +# the second stage of our build just needs the compiled files +FROM openjdk:11-jre +ARG CC_SERVER_PORT=9999 + +# Setup tini to work better handle signals +ENV TINI_VERSION v0.19.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini +RUN chmod +x /tini + +RUN addgroup --system javauser && useradd -g javauser javauser + +# copy only the artifacts we need from the first stage and discard the rest +COPY --chown=javauser:javauser --from=GRADLE_BUILD /home/gradle/build/libs/chaincode.jar /chaincode.jar +COPY --chown=javauser:javauser docker/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh +ENV PORT $CC_SERVER_PORT +EXPOSE $CC_SERVER_PORT + +USER javauser +ENTRYPOINT [ "/tini", "--", "/docker-entrypoint.sh" ] diff --git a/token-erc-20/chaincode-java/config/checkstyle/checkstyle.xml b/token-erc-20/chaincode-java/config/checkstyle/checkstyle.xml old mode 100644 new mode 100755 diff --git a/token-erc-20/chaincode-java/config/checkstyle/suppressions.xml b/token-erc-20/chaincode-java/config/checkstyle/suppressions.xml old mode 100644 new mode 100755 diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractErrors.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractErrors.java index ad6e57a6..4a0a5959 100644 --- a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractErrors.java +++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ContractErrors.java @@ -10,7 +10,7 @@ package org.hyperledger.fabric.samples.erc20; public enum ContractErrors { BALANCE_NOT_FOUND, - UNAUTHERIZED_SENDER, + UNAUTHORIZED_SENDER, INVALID_AMOUNT, NOT_FOUND, INVALID_TRANSFER, diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ERC20TokenContract.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ERC20TokenContract.java index 75c6b2ce..a8ed75c6 100644 --- a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ERC20TokenContract.java +++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/ERC20TokenContract.java @@ -18,9 +18,11 @@ import static org.hyperledger.fabric.samples.erc20.ContractErrors.INVALID_AMOUNT import static org.hyperledger.fabric.samples.erc20.ContractErrors.INVALID_TRANSFER; import static org.hyperledger.fabric.samples.erc20.ContractErrors.NOT_FOUND; import static org.hyperledger.fabric.samples.erc20.ContractErrors.NO_ALLOWANCE_FOUND; -import static org.hyperledger.fabric.samples.erc20.ContractErrors.UNAUTHERIZED_SENDER; +import static org.hyperledger.fabric.samples.erc20.ContractErrors.UNAUTHORIZED_SENDER; import static org.hyperledger.fabric.samples.erc20.utils.ContractUtility.stringIsNullOrEmpty; +import com.owlike.genson.Genson; +import org.hyperledger.fabric.Logger; import org.hyperledger.fabric.contract.Context; import org.hyperledger.fabric.contract.ContractInterface; import org.hyperledger.fabric.contract.annotation.Contact; @@ -54,6 +56,8 @@ import org.hyperledger.fabric.shim.ledger.CompositeKey; @Default public final class ERC20TokenContract implements ContractInterface { + final Logger logger = Logger.getLogger(ERC20TokenContract.class); + /** * Mint creates new tokens and adds them to minter's account balance. This function triggers a * Transfer event. @@ -69,18 +73,15 @@ public final class ERC20TokenContract implements ContractInterface { String clientMSPID = ctx.getClientIdentity().getMSPID(); ChaincodeStub stub = ctx.getStub(); if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) { - throw new ChaincodeException( - "Client is not authorized to mint new tokens", UNAUTHERIZED_SENDER.toString()); + "Client is not authorized to mint new tokens", UNAUTHORIZED_SENDER.toString()); } - // Get ID of submitting client identity String minter = ctx.getClientIdentity().getId(); if (amount <= 0) { throw new ChaincodeException( "Mint amount must be a positive integer", INVALID_AMOUNT.toString()); } - CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter); String currentBalanceStr = stub.getStringState(balanceKey.toString()); // If minter current balance doesn't yet exist, we'll create it with a current balance of 0 @@ -90,7 +91,6 @@ public final class ERC20TokenContract implements ContractInterface { } // Used safe math . long updatedBalance = Math.addExact(currentBalance, amount); - stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance)); // Increase totalSupply String totalSupplyStr = stub.getStringState(TOTAL_SUPPLY_KEY.getValue()); @@ -102,7 +102,11 @@ public final class ERC20TokenContract implements ContractInterface { totalSupply = Math.addExact(totalSupply, amount); stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply)); Transfer transferEvent = new Transfer("0x0", minter, amount); - stub.setEvent(TRANSFER_EVENT.getValue(), transferEvent.toJSONString().getBytes(UTF_8)); + stub.setEvent(TRANSFER_EVENT.getValue(), this.marshal(transferEvent)); + logger.info( + String.format( + "minter account %s balance updated from %d to %d", + minter, currentBalance, updatedBalance)); } /** @@ -121,26 +125,27 @@ public final class ERC20TokenContract implements ContractInterface { ChaincodeStub stub = ctx.getStub(); if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) { throw new ChaincodeException( - "Client is not authorized to burn tokens", UNAUTHERIZED_SENDER.toString()); + "Client is not authorized to burn tokens", UNAUTHORIZED_SENDER.toString()); } - String minter = ctx.getClientIdentity().getId(); - if (amount <= 0) { throw new ChaincodeException( "Burn amount must be a positive integer", INVALID_AMOUNT.toString()); } - CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter); String currentBalanceStr = stub.getStringState(balanceKey.toString()); if (stringIsNullOrEmpty(currentBalanceStr)) { throw new ChaincodeException("The balance does not exist", BALANCE_NOT_FOUND.toString()); } long currentBalance = Long.parseLong(currentBalanceStr); + // Check if the sender has enough tokens to burn. + + if (currentBalance < amount) { + String errorMessage = String.format("Client account %s has insufficient funds", minter); + throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString()); + } long updatedBalance = Math.subtractExact(currentBalance, amount); - stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance)); - // Decrease totalSupply String totalSupplyBytes = stub.getStringState(TOTAL_SUPPLY_KEY.getValue()); if (stringIsNullOrEmpty(totalSupplyBytes)) { @@ -148,10 +153,13 @@ public final class ERC20TokenContract implements ContractInterface { } long totalSupply = Math.subtractExact(Long.parseLong(totalSupplyBytes), amount); stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply)); - // Emit the Transfer event final Transfer transferEvent = new Transfer(minter, "0x0", amount); - stub.setEvent(TRANSFER_EVENT.getValue(), transferEvent.toJSONString().getBytes(UTF_8)); + stub.setEvent(TRANSFER_EVENT.getValue(), this.marshal(transferEvent)); + logger.info( + String.format( + "minter account %s balance updated from %d to %d", + minter, currentBalance, updatedBalance)); } /** @@ -168,7 +176,7 @@ public final class ERC20TokenContract implements ContractInterface { String from = ctx.getClientIdentity().getId(); this.transferHelper(ctx, from, to, value); final Transfer transferEvent = new Transfer(from, to, value); - ctx.getStub().setEvent(TRANSFER_EVENT.getValue(), transferEvent.toJSONString().getBytes(UTF_8)); + ctx.getStub().setEvent(TRANSFER_EVENT.getValue(), this.marshal(transferEvent)); } /** @@ -187,6 +195,7 @@ public final class ERC20TokenContract implements ContractInterface { String errorMessage = String.format("Balance of the owner %s not exists", owner); throw new ChaincodeException(errorMessage, NOT_FOUND.toString()); } + logger.info(String.format("%s has balance of %s tokens", owner, balance)); return Long.parseLong(balance); } @@ -207,7 +216,9 @@ public final class ERC20TokenContract implements ContractInterface { String errorMessage = String.format("The account %s does not exist", clientAccountID); throw new ChaincodeException(errorMessage, NOT_FOUND.toString()); } - return Long.parseLong(balanceBytes); + long balance = Long.parseLong(balanceBytes); + logger.info(String.format("%s has balance of %d tokens", clientAccountID, balance)); + return balance; } /** @@ -235,6 +246,7 @@ public final class ERC20TokenContract implements ContractInterface { if (stringIsNullOrEmpty(totalSupply)) { throw new ChaincodeException("Total Supply not found", NOT_FOUND.toString()); } + logger.info(String.format("TotalSupply: %s tokens", totalSupply)); return Long.parseLong(totalSupply); } @@ -253,7 +265,11 @@ public final class ERC20TokenContract implements ContractInterface { stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender); stub.putStringState(allowanceKey.toString(), String.valueOf(value)); Approval approval = new Approval(owner, spender, value); - stub.setEvent(APPROVAL.getValue(), approval.toJSONString().getBytes(UTF_8)); + stub.setEvent(APPROVAL.getValue(), this.marshal(approval)); + logger.info( + String.format( + "client %s approved a withdrawal allowance of %d for spender %s", + owner, value, spender)); } /** @@ -270,12 +286,15 @@ public final class ERC20TokenContract implements ContractInterface { CompositeKey allowanceKey = stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender); String allowanceBytes = stub.getStringState(allowanceKey.toString()); - if (stringIsNullOrEmpty(allowanceBytes)) { - String errorMessage = - String.format("Spender account %s has no allowance from %s", spender, owner); - throw new ChaincodeException(errorMessage); + long allowance = 0; + if (!stringIsNullOrEmpty(allowanceBytes)) { + allowance = Long.parseLong(allowanceBytes); } - return Long.parseLong(allowanceBytes); + logger.info( + String.format( + "The allowance left for spender %s to withdraw from owner %s: %d", + spender, owner, allowance)); + return allowance; } /** @@ -292,14 +311,12 @@ public final class ERC20TokenContract implements ContractInterface { String spender = ctx.getClientIdentity().getId(); ChaincodeStub stub = ctx.getStub(); // Retrieve the allowance of the spender - CompositeKey allowanceKey = stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), from, spender); String currentAllowanceStr = stub.getStringState(allowanceKey.toString()); if (stringIsNullOrEmpty(currentAllowanceStr)) { String errorMessage = String.format("Spender %s has no allowance from %s", spender, from); throw new ChaincodeException(errorMessage, NO_ALLOWANCE_FOUND.toString()); } - long currentAllowance = Long.parseLong(currentAllowanceStr); // Check if the transferred value is less than the allowance if (currentAllowance < value) { @@ -307,13 +324,16 @@ public final class ERC20TokenContract implements ContractInterface { String.format("Spender %s does not have enough allowance to spend", spender); throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString()); } - this.transferHelper(ctx, from, to, value); // Decrease the allowance long updatedAllowance = currentAllowance - value; stub.putStringState(allowanceKey.toString(), String.valueOf(updatedAllowance)); final Transfer transferEvent = new Transfer(from, to, value); - stub.setEvent(TRANSFER_EVENT.getValue(), transferEvent.toJSONString().getBytes(UTF_8)); + stub.setEvent(TRANSFER_EVENT.getValue(), marshal(transferEvent)); + logger.info( + String.format( + "spender %s allowance updated from %d to %d", + spender, currentAllowance, updatedAllowance)); } /** @@ -344,33 +364,33 @@ public final class ERC20TokenContract implements ContractInterface { String errorMessage = String.format("Client account %s has no balance", from); throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString()); } - long fromCurrentBalance = Long.parseLong(fromCurrentBalanceStr); - // Check if the sender has enough tokens to spend. if (fromCurrentBalance < value) { String errorMessage = String.format("Client account %s has insufficient funds", from); throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString()); } - // Retrieve the current balance of the recipient CompositeKey toBalanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), to); String toCurrentBalanceStr = stub.getStringState(toBalanceKey.toString()); - long toCurrentBalance = 0; // If recipient current balance doesn't yet exist, we'll create it with a // current balance of 0 if (!stringIsNullOrEmpty(toCurrentBalanceStr)) { toCurrentBalance = Long.parseLong(toCurrentBalanceStr.trim()); } - // Update the balance long fromUpdatedBalance = Math.subtractExact(fromCurrentBalance, value); long toUpdatedBalance = Math.addExact(toCurrentBalance, value); - stub.putStringState(fromBalanceKey.toString(), String.valueOf(fromUpdatedBalance)); - stub.putStringState(toBalanceKey.toString(), String.valueOf(toUpdatedBalance)); + logger.info( + String.format( + "client %s balance updated from %d to %d", + from, fromCurrentBalance, fromUpdatedBalance)); + logger.info( + String.format( + "recipient %s balance updated from %d to %d", to, toCurrentBalance, toUpdatedBalance)); } /** @@ -400,13 +420,10 @@ public final class ERC20TokenContract implements ContractInterface { */ @Transaction(intent = Transaction.TYPE.EVALUATE) public String TokenName(final Context ctx) { - String tokenName = ctx.getStub().getStringState(ContractConstants.NAME_KEY.getValue()); - if (stringIsNullOrEmpty(tokenName)) { throw new ChaincodeException("Token name not found", NOT_FOUND.toString()); } - return tokenName; } @@ -422,7 +439,6 @@ public final class ERC20TokenContract implements ContractInterface { if (stringIsNullOrEmpty(tokenSymbol)) { throw new ChaincodeException("Token symbol not found", NOT_FOUND.toString()); } - return tokenSymbol; } @@ -441,4 +457,13 @@ public final class ERC20TokenContract implements ContractInterface { } return Integer.parseInt(decimals); } + /** + * marshal the event data + * + * @param obj the object to marshal. + * @return marshalled object. + */ + private byte[] marshal(final Object obj) { + return new Genson().serialize(obj).getBytes(UTF_8); + } } diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Approval.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Approval.java index e08f0b57..c8226475 100644 --- a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Approval.java +++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Approval.java @@ -1,72 +1,58 @@ -package org.hyperledger.fabric.samples.erc20.model; - -import com.owlike.genson.Genson; -import com.owlike.genson.annotation.JsonProperty; -import org.hyperledger.fabric.contract.annotation.DataType; -import org.hyperledger.fabric.contract.annotation.Property; - -@DataType() -public final class Approval { - - @Property() - @JsonProperty("owner") - private String owner; - - @Property() - @JsonProperty("spender") - private String spender; - - @Property() - @JsonProperty("value") - private long value; - - /** Default constructor */ - public Approval() { - super(); - } - - /** - * Constructor of the class - * - * @param owner token owner - * @param spender approved spender of the token - * @param value amount approved as allowance - */ - public Approval( - @JsonProperty("owner") final String owner, - @JsonProperty("spender") final String spender, - @JsonProperty("value") final long value) { - super(); - this.owner = owner; - this.spender = spender; - this.value = value; - } - - public String getOwner() { - return owner; - } - - public void setOwner(final String owner1) { - this.owner = owner1; - } - - public String getSpender() { - return spender; - } - - public void setSpender(final String spender1) { - this.spender = spender1; - } - - public long getValue() { - return value; - } - - public void setValue(final long value1) { - this.value = value1; - } - - public String toJSONString() { - return new Genson().serialize(this); - } -} +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc20.model; + +import com.owlike.genson.annotation.JsonProperty; +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +@DataType() +public final class Approval { + + @Property() + @JsonProperty("owner") + private String owner; + + @Property() + @JsonProperty("spender") + private String spender; + + @Property() + @JsonProperty("value") + private long value; + + /** Default constructor */ + public Approval() { + super(); + } + + /** + * Constructor of the class + * + * @param owner token owner + * @param spender approved spender of the token + * @param value amount approved as allowance + */ + public Approval( + @JsonProperty("owner") final String owner, + @JsonProperty("spender") final String spender, + @JsonProperty("value") final long value) { + super(); + this.owner = owner; + this.spender = spender; + this.value = value; + } + + public String getOwner() { + return owner; + } + + public String getSpender() { + return spender; + } + + public long getValue() { + return value; + } +} diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Transfer.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Transfer.java index 55d5fb64..0e7275c6 100644 --- a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Transfer.java +++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/model/Transfer.java @@ -1,73 +1,58 @@ -package org.hyperledger.fabric.samples.erc20.model; - -import com.owlike.genson.Genson; -import com.owlike.genson.annotation.JsonProperty; -import org.hyperledger.fabric.contract.annotation.DataType; -import org.hyperledger.fabric.contract.annotation.Property; - -@DataType() -public final class Transfer { - - @Property() - @JsonProperty("from") - private String from; - - @Property() - @JsonProperty("to") - private String to; - - @Property() - @JsonProperty("value") - private long value; - - /** Default constructor */ - public Transfer() { - super(); - } - - /** - * Constructor of the class - * - * @param from owner of the token - * @param to token receiver - * @param value amount to be transferred - */ - public Transfer( - @JsonProperty("from") final String from, - @JsonProperty("to") final String to, - @JsonProperty("value") final long value) { - super(); - this.from = from; - this.to = to; - this.value = value; - } - - public String getFrom() { - return from; - } - - public void setFrom(final String from1) { - this.from = from1; - } - - public String getTo() { - return to; - } - - public void setTo(final String to1) { - this.to = to1; - } - - public long getValue() { - return value; - } - - public void setValue(final long value1) { - this.value = value1; - } - - /** @return String JSON */ - public String toJSONString() { - return new Genson().serialize(this); - } -} +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.erc20.model; + +import com.owlike.genson.annotation.JsonProperty; +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +@DataType() +public final class Transfer { + + @Property() + @JsonProperty("from") + private String from; + + @Property() + @JsonProperty("to") + private String to; + + @Property() + @JsonProperty("value") + private long value; + + /** Default constructor */ + public Transfer() { + super(); + } + + /** + * Constructor of the class + * + * @param from owner of the token + * @param to token receiver + * @param value amount to be transferred + */ + public Transfer( + @JsonProperty("from") final String from, + @JsonProperty("to") final String to, + @JsonProperty("value") final long value) { + super(); + this.from = from; + this.to = to; + this.value = value; + } + + public String getFrom() { + return from; + } + + public String getTo() { + return to; + } + + public long getValue() { + return value; + } +} diff --git a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/utils/ContractUtility.java b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/utils/ContractUtility.java index 1ec8738b..bf952314 100644 --- a/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/utils/ContractUtility.java +++ b/token-erc-20/chaincode-java/src/main/java/org/hyperledger/fabric/samples/erc20/utils/ContractUtility.java @@ -12,6 +12,7 @@ public final class ContractUtility { private ContractUtility() { } + public static boolean stringIsNullOrEmpty(final String string) { return string == null || string.isEmpty(); } diff --git a/token-erc-20/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc20/TokenERC20ContractTest.java b/token-erc-20/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc20/TokenERC20ContractTest.java index d9186f7b..a38ac986 100644 --- a/token-erc-20/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc20/TokenERC20ContractTest.java +++ b/token-erc-20/chaincode-java/src/test/java/org/hyperledger/fabric/samples/erc20/TokenERC20ContractTest.java @@ -1,3 +1,6 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ package org.hyperledger.fabric.samples.erc20; import org.hyperledger.fabric.contract.ClientIdentity; @@ -333,6 +336,37 @@ public class TokenERC20ContractTest { .hasMessage("Transfer amount cannot be negative"); } + @Test + public void whenTokenTransferSameId() { + + ERC20TokenContract contract = new ERC20TokenContract(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + ClientIdentity ci = mock(ClientIdentity.class); + when(ctx.getClientIdentity()).thenReturn(ci); + when(ci.getMSPID()).thenReturn(MINTER_ORG_MSPID.getValue()); + when(ci.getId()).thenReturn(org1UserId); + when(ctx.getStub()).thenReturn(stub); + String to = + "x509::CN=User2@org2.example.com, L=San Francisco, ST=California," + + " C=US::CN=ca.org2.example.com, O=org2.example.com, L=San Francisco, ST=California, C=US"; + + CompositeKey ckFrom = mock(CompositeKey.class); + when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ckFrom); + when(ckFrom.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId); + when(stub.getStringState(ckFrom.toString())).thenReturn("1000"); + CompositeKey ckTo = mock(CompositeKey.class); + when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), to)).thenReturn(ckTo); + when(ckTo.toString()).thenReturn(BALANCE_PREFIX.getValue() + to); + when(stub.getStringState(ckTo.toString())).thenReturn(null); + + Throwable thrown = catchThrowable(() -> contract.Transfer(ctx, org1UserId, 10)); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("Cannot transfer to and from same client account"); + } + @Test public void invokeTokenBurnTest() { @@ -362,10 +396,10 @@ public class TokenERC20ContractTest { ClientIdentity ci = mock(ClientIdentity.class); when(ctx.getClientIdentity()).thenReturn(ci); when(ci.getMSPID()).thenReturn("Org2MSP"); - when(ci.getId()).thenReturn(org1UserId); + when(ci.getId()).thenReturn(spender); CompositeKey ck = mock(CompositeKey.class); - when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck); - when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId); + when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), spender)).thenReturn(ck); + when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + spender); when(stub.getStringState(ck.toString())).thenReturn(null); when(ctx.getStub()).thenReturn(stub); when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("1000"); @@ -377,6 +411,30 @@ public class TokenERC20ContractTest { .hasNoCause() .hasMessage("Client is not authorized to burn tokens"); } + + @Test + public void whenTokenBurnNegativeAmountTest() { + ERC20TokenContract contract = new ERC20TokenContract(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + ClientIdentity ci = mock(ClientIdentity.class); + when(ctx.getClientIdentity()).thenReturn(ci); + when(ci.getMSPID()).thenReturn("Org1MSP"); + when(ci.getId()).thenReturn(org1UserId); + CompositeKey ck = mock(CompositeKey.class); + when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck); + when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId); + when(stub.getStringState(ck.toString())).thenReturn(null); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("1000"); + when(stub.getStringState(ck.toString())).thenReturn("1000"); + + Throwable thrown = catchThrowable(() -> contract.Burn(ctx, -100)); + assertThat(thrown) + .isInstanceOf(ChaincodeException.class) + .hasNoCause() + .hasMessage("Burn amount must be a positive integer"); + } } @Nested