Java erc20 token standard chaincode implementation Signed-off-by: renjithpta <renjithkn@gmail.com>

Signed-off-by: renjithpta <renjithkn@gmail.com>
This commit is contained in:
renjithpta 2022-04-19 07:27:10 +00:00
parent 006c3fa3b4
commit 24c296c8a8
19 changed files with 323 additions and 239 deletions

View file

@ -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. Jobs are not used to get assets, because evaluating transactions is typically much faster.
Related files: Related files:
- [src/asset.router.ts](src/asset.router.ts) - [src/assets.router.ts](src/assets.router.ts)
Defines the main `/api/assets` endpoint. Defines the main `/api/assets` endpoint.
- [src/fabric.ts](src/fabric.ts) - [src/fabric.ts](src/fabric.ts)
All the sample code which interacts with the Fabric network via the Fabric SDK. 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. Defines the `/api/jobs` endpoint for getting job status.
- [src/jobs.ts](src/jobs.ts) - [src/jobs.ts](src/jobs.ts)
Job queue implementation details. 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. 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. **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.

View file

@ -5570,9 +5570,9 @@
"dev": true "dev": true
}, },
"node_modules/moment": { "node_modules/moment": {
"version": "2.29.1", "version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
"engines": { "engines": {
"node": "*" "node": "*"
} }
@ -11993,9 +11993,9 @@
"dev": true "dev": true
}, },
"moment": { "moment": {
"version": "2.29.1", "version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
}, },
"moment-timezone": { "moment-timezone": {
"version": "0.5.33", "version": "0.5.33",

View file

@ -72,7 +72,7 @@ simplified, but realistic CA deployment illustrating key touch points with Kuber
### Future Enhancements: ### 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 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) 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. and/or alternate signing chains backed by formal (e.g. letsencrypt, Thawte, Verisign, etc.) certificate authorities.

View file

@ -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. 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 ## Aggregating the Channel MSP

View file

@ -4,7 +4,7 @@ The peers have been configured so they implemented a essential failover/high-ava
Two important notes: 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. 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 ## 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 ## 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. "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. For this KIND cluster, this is configured by updating the cluster configuration, add the following yaml.

BIN
test-network-k8s/kubectl Normal file

Binary file not shown.

View file

@ -174,6 +174,11 @@ elif [ "${MODE}" == "chaincode" ]; then
elif [ "${MODE}" == "anchor" ]; then elif [ "${MODE}" == "anchor" ]; then
update_anchor_peers $@ 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 elif [ "${MODE}" == "rest-easy" ]; then
log "Launching fabric-rest-sample application:" log "Launching fabric-rest-sample application:"
launch_rest_sample launch_rest_sample

View file

@ -13,6 +13,7 @@ function pull_docker_images() {
docker pull ${FABRIC_CONTAINER_REGISTRY}/fabric-peer:$FABRIC_VERSION docker pull ${FABRIC_CONTAINER_REGISTRY}/fabric-peer:$FABRIC_VERSION
docker pull ${FABRIC_CONTAINER_REGISTRY}/fabric-tools:$FABRIC_VERSION docker pull ${FABRIC_CONTAINER_REGISTRY}/fabric-tools:$FABRIC_VERSION
docker pull ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic:latest docker pull ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic:latest
docker pull couchdb:3.2.1
pop_fn pop_fn
} }
@ -30,6 +31,25 @@ function load_docker_images() {
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() { function apply_nginx_ingress() {
push_fn "Launching ingress controller" push_fn "Launching ingress controller"
@ -183,6 +203,8 @@ function kind_init() {
if [ "${STAGE_DOCKER_IMAGES}" == true ]; then if [ "${STAGE_DOCKER_IMAGES}" == true ]; then
pull_docker_images pull_docker_images
load_docker_images load_docker_images
pull_docker_images_for_rest_sample
load_docker_images_for_rest_sample
fi fi
wait_for_cert_manager wait_for_cert_manager

View file

@ -23,7 +23,7 @@ function yaml_ccp {
-e "s/\${CAPORT}/$3/" \ -e "s/\${CAPORT}/$3/" \
-e "s#\${PEERPEM}#$PP#" \ -e "s#\${PEERPEM}#$PP#" \
-e "s#\${CAPEM}#$CP#" \ -e "s#\${CAPEM}#$CP#" \
ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g' ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g'
} }
ORG=3 ORG=3

View file

@ -19,7 +19,7 @@ peers:
url: grpcs://localhost:${P0PORT} url: grpcs://localhost:${P0PORT}
tlsCACerts: tlsCACerts:
pem: | pem: |
${PEERPEM} ${PEERPEM}
grpcOptions: grpcOptions:
ssl-target-name-override: peer0.org${ORG}.example.com ssl-target-name-override: peer0.org${ORG}.example.com
hostnameOverride: peer0.org${ORG}.example.com hostnameOverride: peer0.org${ORG}.example.com
@ -28,7 +28,8 @@ certificateAuthorities:
url: https://localhost:${CAPORT} url: https://localhost:${CAPORT}
caName: ca-org${ORG} caName: ca-org${ORG}
tlsCACerts: tlsCACerts:
pem: | pem:
${CAPEM} - |
${CAPEM}
httpOptions: httpOptions:
verify: false verify: false

View file

@ -1,3 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
# the first stage # the first stage
FROM gradle:jdk11 AS GRADLE_BUILD FROM gradle:jdk11 AS GRADLE_BUILD

View file

View file

View file

@ -10,7 +10,7 @@ package org.hyperledger.fabric.samples.erc20;
public enum ContractErrors { public enum ContractErrors {
BALANCE_NOT_FOUND, BALANCE_NOT_FOUND,
UNAUTHERIZED_SENDER, UNAUTHORIZED_SENDER,
INVALID_AMOUNT, INVALID_AMOUNT,
NOT_FOUND, NOT_FOUND,
INVALID_TRANSFER, INVALID_TRANSFER,

View file

@ -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.INVALID_TRANSFER;
import static org.hyperledger.fabric.samples.erc20.ContractErrors.NOT_FOUND; 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.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 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.Context;
import org.hyperledger.fabric.contract.ContractInterface; import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.Contact; import org.hyperledger.fabric.contract.annotation.Contact;
@ -54,6 +56,8 @@ import org.hyperledger.fabric.shim.ledger.CompositeKey;
@Default @Default
public final class ERC20TokenContract implements ContractInterface { 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 * Mint creates new tokens and adds them to minter's account balance. This function triggers a
* Transfer event. * Transfer event.
@ -69,18 +73,15 @@ public final class ERC20TokenContract implements ContractInterface {
String clientMSPID = ctx.getClientIdentity().getMSPID(); String clientMSPID = ctx.getClientIdentity().getMSPID();
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) { if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) {
throw new ChaincodeException( 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 // Get ID of submitting client identity
String minter = ctx.getClientIdentity().getId(); String minter = ctx.getClientIdentity().getId();
if (amount <= 0) { if (amount <= 0) {
throw new ChaincodeException( throw new ChaincodeException(
"Mint amount must be a positive integer", INVALID_AMOUNT.toString()); "Mint amount must be a positive integer", INVALID_AMOUNT.toString());
} }
CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter); CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter);
String currentBalanceStr = stub.getStringState(balanceKey.toString()); String currentBalanceStr = stub.getStringState(balanceKey.toString());
// If minter current balance doesn't yet exist, we'll create it with a current balance of 0 // 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 . // Used safe math .
long updatedBalance = Math.addExact(currentBalance, amount); long updatedBalance = Math.addExact(currentBalance, amount);
stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance)); stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance));
// Increase totalSupply // Increase totalSupply
String totalSupplyStr = stub.getStringState(TOTAL_SUPPLY_KEY.getValue()); String totalSupplyStr = stub.getStringState(TOTAL_SUPPLY_KEY.getValue());
@ -102,7 +102,11 @@ public final class ERC20TokenContract implements ContractInterface {
totalSupply = Math.addExact(totalSupply, amount); totalSupply = Math.addExact(totalSupply, amount);
stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply)); stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply));
Transfer transferEvent = new Transfer("0x0", minter, amount); 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(); ChaincodeStub stub = ctx.getStub();
if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) { if (!clientMSPID.equalsIgnoreCase(ContractConstants.MINTER_ORG_MSPID.getValue())) {
throw new ChaincodeException( 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(); String minter = ctx.getClientIdentity().getId();
if (amount <= 0) { if (amount <= 0) {
throw new ChaincodeException( throw new ChaincodeException(
"Burn amount must be a positive integer", INVALID_AMOUNT.toString()); "Burn amount must be a positive integer", INVALID_AMOUNT.toString());
} }
CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter); CompositeKey balanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), minter);
String currentBalanceStr = stub.getStringState(balanceKey.toString()); String currentBalanceStr = stub.getStringState(balanceKey.toString());
if (stringIsNullOrEmpty(currentBalanceStr)) { if (stringIsNullOrEmpty(currentBalanceStr)) {
throw new ChaincodeException("The balance does not exist", BALANCE_NOT_FOUND.toString()); throw new ChaincodeException("The balance does not exist", BALANCE_NOT_FOUND.toString());
} }
long currentBalance = Long.parseLong(currentBalanceStr); 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); long updatedBalance = Math.subtractExact(currentBalance, amount);
stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance)); stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance));
// Decrease totalSupply // Decrease totalSupply
String totalSupplyBytes = stub.getStringState(TOTAL_SUPPLY_KEY.getValue()); String totalSupplyBytes = stub.getStringState(TOTAL_SUPPLY_KEY.getValue());
if (stringIsNullOrEmpty(totalSupplyBytes)) { if (stringIsNullOrEmpty(totalSupplyBytes)) {
@ -148,10 +153,13 @@ public final class ERC20TokenContract implements ContractInterface {
} }
long totalSupply = Math.subtractExact(Long.parseLong(totalSupplyBytes), amount); long totalSupply = Math.subtractExact(Long.parseLong(totalSupplyBytes), amount);
stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply)); stub.putStringState(TOTAL_SUPPLY_KEY.getValue(), String.valueOf(totalSupply));
// Emit the Transfer event // Emit the Transfer event
final Transfer transferEvent = new Transfer(minter, "0x0", amount); 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(); String from = ctx.getClientIdentity().getId();
this.transferHelper(ctx, from, to, value); this.transferHelper(ctx, from, to, value);
final Transfer transferEvent = new Transfer(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); String errorMessage = String.format("Balance of the owner %s not exists", owner);
throw new ChaincodeException(errorMessage, NOT_FOUND.toString()); throw new ChaincodeException(errorMessage, NOT_FOUND.toString());
} }
logger.info(String.format("%s has balance of %s tokens", owner, balance));
return Long.parseLong(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); String errorMessage = String.format("The account %s does not exist", clientAccountID);
throw new ChaincodeException(errorMessage, NOT_FOUND.toString()); 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)) { if (stringIsNullOrEmpty(totalSupply)) {
throw new ChaincodeException("Total Supply not found", NOT_FOUND.toString()); throw new ChaincodeException("Total Supply not found", NOT_FOUND.toString());
} }
logger.info(String.format("TotalSupply: %s tokens", totalSupply));
return Long.parseLong(totalSupply); return Long.parseLong(totalSupply);
} }
@ -253,7 +265,11 @@ public final class ERC20TokenContract implements ContractInterface {
stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender); stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender);
stub.putStringState(allowanceKey.toString(), String.valueOf(value)); stub.putStringState(allowanceKey.toString(), String.valueOf(value));
Approval approval = new Approval(owner, spender, 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 = CompositeKey allowanceKey =
stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender); stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), owner, spender);
String allowanceBytes = stub.getStringState(allowanceKey.toString()); String allowanceBytes = stub.getStringState(allowanceKey.toString());
if (stringIsNullOrEmpty(allowanceBytes)) { long allowance = 0;
String errorMessage = if (!stringIsNullOrEmpty(allowanceBytes)) {
String.format("Spender account %s has no allowance from %s", spender, owner); allowance = Long.parseLong(allowanceBytes);
throw new ChaincodeException(errorMessage);
} }
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(); String spender = ctx.getClientIdentity().getId();
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
// Retrieve the allowance of the spender // Retrieve the allowance of the spender
CompositeKey allowanceKey = stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), from, spender); CompositeKey allowanceKey = stub.createCompositeKey(ALLOWANCE_PREFIX.getValue(), from, spender);
String currentAllowanceStr = stub.getStringState(allowanceKey.toString()); String currentAllowanceStr = stub.getStringState(allowanceKey.toString());
if (stringIsNullOrEmpty(currentAllowanceStr)) { if (stringIsNullOrEmpty(currentAllowanceStr)) {
String errorMessage = String.format("Spender %s has no allowance from %s", spender, from); String errorMessage = String.format("Spender %s has no allowance from %s", spender, from);
throw new ChaincodeException(errorMessage, NO_ALLOWANCE_FOUND.toString()); throw new ChaincodeException(errorMessage, NO_ALLOWANCE_FOUND.toString());
} }
long currentAllowance = Long.parseLong(currentAllowanceStr); long currentAllowance = Long.parseLong(currentAllowanceStr);
// Check if the transferred value is less than the allowance // Check if the transferred value is less than the allowance
if (currentAllowance < value) { 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); String.format("Spender %s does not have enough allowance to spend", spender);
throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString()); throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString());
} }
this.transferHelper(ctx, from, to, value); this.transferHelper(ctx, from, to, value);
// Decrease the allowance // Decrease the allowance
long updatedAllowance = currentAllowance - value; long updatedAllowance = currentAllowance - value;
stub.putStringState(allowanceKey.toString(), String.valueOf(updatedAllowance)); stub.putStringState(allowanceKey.toString(), String.valueOf(updatedAllowance));
final Transfer transferEvent = new Transfer(from, to, value); 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); String errorMessage = String.format("Client account %s has no balance", from);
throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString()); throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString());
} }
long fromCurrentBalance = Long.parseLong(fromCurrentBalanceStr); long fromCurrentBalance = Long.parseLong(fromCurrentBalanceStr);
// Check if the sender has enough tokens to spend. // Check if the sender has enough tokens to spend.
if (fromCurrentBalance < value) { if (fromCurrentBalance < value) {
String errorMessage = String.format("Client account %s has insufficient funds", from); String errorMessage = String.format("Client account %s has insufficient funds", from);
throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString()); throw new ChaincodeException(errorMessage, INSUFFICIENT_FUND.toString());
} }
// Retrieve the current balance of the recipient // Retrieve the current balance of the recipient
CompositeKey toBalanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), to); CompositeKey toBalanceKey = stub.createCompositeKey(BALANCE_PREFIX.getValue(), to);
String toCurrentBalanceStr = stub.getStringState(toBalanceKey.toString()); String toCurrentBalanceStr = stub.getStringState(toBalanceKey.toString());
long toCurrentBalance = 0; long toCurrentBalance = 0;
// If recipient current balance doesn't yet exist, we'll create it with a // If recipient current balance doesn't yet exist, we'll create it with a
// current balance of 0 // current balance of 0
if (!stringIsNullOrEmpty(toCurrentBalanceStr)) { if (!stringIsNullOrEmpty(toCurrentBalanceStr)) {
toCurrentBalance = Long.parseLong(toCurrentBalanceStr.trim()); toCurrentBalance = Long.parseLong(toCurrentBalanceStr.trim());
} }
// Update the balance // Update the balance
long fromUpdatedBalance = Math.subtractExact(fromCurrentBalance, value); long fromUpdatedBalance = Math.subtractExact(fromCurrentBalance, value);
long toUpdatedBalance = Math.addExact(toCurrentBalance, value); long toUpdatedBalance = Math.addExact(toCurrentBalance, value);
stub.putStringState(fromBalanceKey.toString(), String.valueOf(fromUpdatedBalance)); stub.putStringState(fromBalanceKey.toString(), String.valueOf(fromUpdatedBalance));
stub.putStringState(toBalanceKey.toString(), String.valueOf(toUpdatedBalance)); 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) @Transaction(intent = Transaction.TYPE.EVALUATE)
public String TokenName(final Context ctx) { public String TokenName(final Context ctx) {
String tokenName = ctx.getStub().getStringState(ContractConstants.NAME_KEY.getValue()); String tokenName = ctx.getStub().getStringState(ContractConstants.NAME_KEY.getValue());
if (stringIsNullOrEmpty(tokenName)) { if (stringIsNullOrEmpty(tokenName)) {
throw new ChaincodeException("Token name not found", NOT_FOUND.toString()); throw new ChaincodeException("Token name not found", NOT_FOUND.toString());
} }
return tokenName; return tokenName;
} }
@ -422,7 +439,6 @@ public final class ERC20TokenContract implements ContractInterface {
if (stringIsNullOrEmpty(tokenSymbol)) { if (stringIsNullOrEmpty(tokenSymbol)) {
throw new ChaincodeException("Token symbol not found", NOT_FOUND.toString()); throw new ChaincodeException("Token symbol not found", NOT_FOUND.toString());
} }
return tokenSymbol; return tokenSymbol;
} }
@ -441,4 +457,13 @@ public final class ERC20TokenContract implements ContractInterface {
} }
return Integer.parseInt(decimals); 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);
}
} }

View file

@ -1,6 +1,8 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.fabric.samples.erc20.model; package org.hyperledger.fabric.samples.erc20.model;
import com.owlike.genson.Genson;
import com.owlike.genson.annotation.JsonProperty; import com.owlike.genson.annotation.JsonProperty;
import org.hyperledger.fabric.contract.annotation.DataType; import org.hyperledger.fabric.contract.annotation.DataType;
import org.hyperledger.fabric.contract.annotation.Property; import org.hyperledger.fabric.contract.annotation.Property;
@ -46,27 +48,11 @@ public final class Approval {
return owner; return owner;
} }
public void setOwner(final String owner1) {
this.owner = owner1;
}
public String getSpender() { public String getSpender() {
return spender; return spender;
} }
public void setSpender(final String spender1) {
this.spender = spender1;
}
public long getValue() { public long getValue() {
return value; return value;
} }
public void setValue(final long value1) {
this.value = value1;
}
public String toJSONString() {
return new Genson().serialize(this);
}
} }

View file

@ -1,6 +1,8 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.fabric.samples.erc20.model; package org.hyperledger.fabric.samples.erc20.model;
import com.owlike.genson.Genson;
import com.owlike.genson.annotation.JsonProperty; import com.owlike.genson.annotation.JsonProperty;
import org.hyperledger.fabric.contract.annotation.DataType; import org.hyperledger.fabric.contract.annotation.DataType;
import org.hyperledger.fabric.contract.annotation.Property; import org.hyperledger.fabric.contract.annotation.Property;
@ -46,28 +48,11 @@ public final class Transfer {
return from; return from;
} }
public void setFrom(final String from1) {
this.from = from1;
}
public String getTo() { public String getTo() {
return to; return to;
} }
public void setTo(final String to1) {
this.to = to1;
}
public long getValue() { public long getValue() {
return value; return value;
} }
public void setValue(final long value1) {
this.value = value1;
}
/** @return String JSON */
public String toJSONString() {
return new Genson().serialize(this);
}
} }

View file

@ -12,6 +12,7 @@ public final class ContractUtility {
private ContractUtility() { private ContractUtility() {
} }
public static boolean stringIsNullOrEmpty(final String string) { public static boolean stringIsNullOrEmpty(final String string) {
return string == null || string.isEmpty(); return string == null || string.isEmpty();
} }

View file

@ -1,3 +1,6 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.fabric.samples.erc20; package org.hyperledger.fabric.samples.erc20;
import org.hyperledger.fabric.contract.ClientIdentity; import org.hyperledger.fabric.contract.ClientIdentity;
@ -333,6 +336,37 @@ public class TokenERC20ContractTest {
.hasMessage("Transfer amount cannot be negative"); .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 @Test
public void invokeTokenBurnTest() { public void invokeTokenBurnTest() {
@ -362,10 +396,10 @@ public class TokenERC20ContractTest {
ClientIdentity ci = mock(ClientIdentity.class); ClientIdentity ci = mock(ClientIdentity.class);
when(ctx.getClientIdentity()).thenReturn(ci); when(ctx.getClientIdentity()).thenReturn(ci);
when(ci.getMSPID()).thenReturn("Org2MSP"); when(ci.getMSPID()).thenReturn("Org2MSP");
when(ci.getId()).thenReturn(org1UserId); when(ci.getId()).thenReturn(spender);
CompositeKey ck = mock(CompositeKey.class); CompositeKey ck = mock(CompositeKey.class);
when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), org1UserId)).thenReturn(ck); when(stub.createCompositeKey(BALANCE_PREFIX.getValue(), spender)).thenReturn(ck);
when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + org1UserId); when(ck.toString()).thenReturn(BALANCE_PREFIX.getValue() + spender);
when(stub.getStringState(ck.toString())).thenReturn(null); when(stub.getStringState(ck.toString())).thenReturn(null);
when(ctx.getStub()).thenReturn(stub); when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("1000"); when(stub.getStringState(TOTAL_SUPPLY_KEY.getValue())).thenReturn("1000");
@ -377,6 +411,30 @@ public class TokenERC20ContractTest {
.hasNoCause() .hasNoCause()
.hasMessage("Client is not authorized to burn tokens"); .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 @Nested