mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-23 01:55:10 +00:00
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:
parent
006c3fa3b4
commit
24c296c8a8
19 changed files with 323 additions and 239 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
BIN
test-network-k8s/kubectl
Normal file
BIN
test-network-k8s/kubectl
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" ]
|
||||
|
|
|
|||
0
token-erc-20/chaincode-java/config/checkstyle/checkstyle.xml
Normal file → Executable file
0
token-erc-20/chaincode-java/config/checkstyle/checkstyle.xml
Normal file → Executable file
0
token-erc-20/chaincode-java/config/checkstyle/suppressions.xml
Normal file → Executable file
0
token-erc-20/chaincode-java/config/checkstyle/suppressions.xml
Normal file → Executable file
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public final class ContractUtility {
|
|||
private ContractUtility() {
|
||||
}
|
||||
|
||||
|
||||
public static boolean stringIsNullOrEmpty(final String string) {
|
||||
return string == null || string.isEmpty();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue