mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-26 03:25:09 +00:00
ERC20 Java chaincode implementation exmaple.
Signed-off-by: renjithkn@gmail.com <renjithkn@gmail.com>
This commit is contained in:
parent
77431f5b39
commit
590851471b
4 changed files with 1362 additions and 0 deletions
165
token-erc-20/chaincode-java/pom.xml
Normal file
165
token-erc-20/chaincode-java/pom.xml
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>TokenERC20Contract</groupId>
|
||||
<artifactId>TokenERC20Contract</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<properties>
|
||||
|
||||
<!-- Generic properties -->
|
||||
<java.version>1.8</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
||||
<!-- fabric-chaincode-java -->
|
||||
<fabric-chaincode-java.version>2.2.0</fabric-chaincode-java.version>
|
||||
|
||||
<!-- Logging -->
|
||||
<logback.version>1.0.13</logback.version>
|
||||
<slf4j.version>1.7.5</slf4j.version>
|
||||
|
||||
<!-- Test -->
|
||||
<junit.jupiter.version>5.4.2</junit.jupiter.version>
|
||||
<junit.platform.version>1.3.0-RC1</junit.platform.version>
|
||||
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://www.jitpack.io</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>artifactory</id>
|
||||
<url>https://hyperledger.jfrog.io/hyperledger/fabric-maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- fabric-chaincode-java -->
|
||||
<dependency>
|
||||
<groupId>org.hyperledger.fabric-chaincode-java</groupId>
|
||||
<artifactId>fabric-chaincode-shim</artifactId>
|
||||
<version>${fabric-chaincode-java.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hyperledger.fabric-chaincode-java</groupId>
|
||||
<artifactId>fabric-chaincode-protos</artifactId>
|
||||
<version>${fabric-chaincode-java.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- fabric-sdk-java -->
|
||||
|
||||
<!-- Logging with SLF4J & LogBack -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Artifacts -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.11.1</version>
|
||||
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>2.23.0</version>
|
||||
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.json/json -->
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20180813</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
<plugins>
|
||||
<!-- JUnit 5 requires Surefire version 2.22.0 or higher -->
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>chaincode</finalName>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.hyperledger.fabric.contract.ContractRouter</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<!-- filter out signature files from signed dependencies, else repackaging fails with security ex -->
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,519 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.example;
|
||||
|
||||
|
||||
import org.hyperledger.fabric.contract.Context;
|
||||
import org.hyperledger.fabric.contract.ContractInterface;
|
||||
import org.hyperledger.fabric.contract.annotation.*;
|
||||
import org.hyperledger.fabric.shim.ChaincodeException;
|
||||
import org.hyperledger.fabric.shim.ChaincodeStub;
|
||||
import org.hyperledger.fabric.shim.ledger.CompositeKey;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
@Contract(name = "TokenERC20Contract", info = @Info(title = "TokenERC20Contract", description = "A java chaincode for erc20 token", version = "0.0.1-SNAPSHOT"))
|
||||
|
||||
@Default
|
||||
public final class TokenERC20Contract implements ContractInterface {
|
||||
|
||||
|
||||
final private String balancePrefix = "balance";
|
||||
final private String allowancePrefix = "allowance";
|
||||
final private String nameKey = "name";
|
||||
final private String symbolKey = "symbol";
|
||||
final private String decimalsKey = "decimals";
|
||||
final private String totalSupplyKey = "totalSupply";
|
||||
|
||||
/**
|
||||
* Return the name of the token - e.g. "MyToken". The original function name is
|
||||
* `name` in ERC20 specification. However, 'name' conflicts with a parameter
|
||||
* `name` in `Contract` class. As a work around, we use `TokenName` as an
|
||||
* alternative function name.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {String} Returns the name of the token
|
||||
*/
|
||||
@Transaction()
|
||||
public String tokenName(final Context ctx) {
|
||||
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
String tokenName = stub.getStringState(nameKey);
|
||||
if (tokenName.isEmpty()) {
|
||||
|
||||
throw new ChaincodeException("Sorry ! Token name not found");
|
||||
}
|
||||
|
||||
return tokenName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the symbol of the token. E.g. “HIX”.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {String} Returns the symbol of the token
|
||||
*/
|
||||
@Transaction()
|
||||
public String tokenSymbol(final Context ctx) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
String tokenSymbol = stub.getStringState(symbolKey);
|
||||
if (tokenSymbol.isEmpty()) {
|
||||
|
||||
throw new ChaincodeException("Sorry ! Token symbol not found");
|
||||
}
|
||||
|
||||
return tokenSymbol;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of decimals the token uses e.g. 8, means to divide the
|
||||
* token amount by 100000000 to get its user representation.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {Number} Returns the number of decimals
|
||||
*/
|
||||
@Transaction()
|
||||
public Integer decimals(final Context ctx) {
|
||||
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
String decimals = stub.getStringState(decimalsKey);
|
||||
if (decimals.isEmpty()) {
|
||||
|
||||
throw new ChaincodeException("Sorry ! Decimal not found");
|
||||
}
|
||||
return Integer.parseInt(decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total token supply.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {Number} Returns the total token supply
|
||||
*/
|
||||
@Transaction()
|
||||
public Long totalSupply(final Context ctx) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
String totalSupply = stub.getStringState(totalSupplyKey);
|
||||
if (totalSupply.isEmpty()) {
|
||||
|
||||
throw new ChaincodeException("Sorry ! Total Supply not found");
|
||||
}
|
||||
return Long.parseLong(totalSupply);
|
||||
}
|
||||
|
||||
/**
|
||||
* BalanceOf returns the balance of the given account.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} owner The owner from which the balance will be retrieved
|
||||
* @returns {Number} Returns the account balance
|
||||
*/
|
||||
@Transaction()
|
||||
public long balanceOf(final Context ctx, final String owner) {
|
||||
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
CompositeKey balanceKey = stub.createCompositeKey(balancePrefix, owner);
|
||||
|
||||
String balance = stub.getStringState(balanceKey.toString().trim());
|
||||
if (balance == null || balance.isEmpty() || balance.length() == 0) {
|
||||
String errorMessage = String.format("Balance of the owner %s not exists", owner);
|
||||
System.out.println(errorMessage);
|
||||
throw new ChaincodeException(errorMessage);
|
||||
}
|
||||
|
||||
return Long.parseLong(balance.toString());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer transfers tokens from client account to recipient account. recipient
|
||||
* account must be a valid clientID as returned by the ClientAccountID()
|
||||
* function.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} to The recipient
|
||||
* @param {Integer} value The amount of token to be transferred
|
||||
* @returns {Boolean} Return whether the transfer was successful or not
|
||||
*/
|
||||
@Transaction()
|
||||
public boolean transfer(final Context ctx, final String to, String _value) {
|
||||
|
||||
String from = ctx.getClientIdentity().getId();
|
||||
long value = Long.parseLong(_value.trim());
|
||||
boolean transferResp = this.doTransfer(ctx, from, to, value);
|
||||
|
||||
if (!transferResp) {
|
||||
String errorMessage = String.format("Cannot transfer to and from same client account");
|
||||
System.out.println(errorMessage);
|
||||
throw new ChaincodeException(errorMessage);
|
||||
}
|
||||
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("from", from);
|
||||
obj.put("to", to);
|
||||
obj.put("value", value);
|
||||
stub.setEvent("Transfer", this.serialize(obj));
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer `value` amount of tokens from `from` to `to`.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} from The sender
|
||||
* @param {String} to The recipient
|
||||
* @param {Integer} value The amount of token to be transferred
|
||||
* @returns {Boolean} Return whether the transfer was successful or not
|
||||
*/
|
||||
@Transaction()
|
||||
public boolean transferFrom(Context ctx, final String from, final String to, String _value) {
|
||||
|
||||
String spender = ctx.getClientIdentity().getId();
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
// Retrieve the allowance of the spender
|
||||
CompositeKey allowanceKey = stub.createCompositeKey(allowancePrefix, from, spender);
|
||||
String currentAllowanceStr = stub.getStringState(allowanceKey.toString().trim());
|
||||
if (currentAllowanceStr.isBlank() || currentAllowanceStr.length() == 0) {
|
||||
String errorMessage = String.format("Spender %s has no allowance from %s", spender, from);
|
||||
System.out.println(errorMessage);
|
||||
throw new ChaincodeException(errorMessage);
|
||||
}
|
||||
long currentAllowance = Long.parseLong(currentAllowanceStr.toString());
|
||||
|
||||
// Convert value from string to int
|
||||
Long valueInt = Long.parseLong(_value);
|
||||
|
||||
// Check if the transferred value is less than the allowance
|
||||
if (currentAllowance < valueInt) {
|
||||
|
||||
String errorMessage = String.format("The spender does not have enough allowance to spend.");
|
||||
System.out.println(errorMessage);
|
||||
throw new ChaincodeException(errorMessage);
|
||||
|
||||
}
|
||||
|
||||
boolean transferResp = this.doTransfer(ctx, from, to, valueInt);
|
||||
|
||||
if (!transferResp) {
|
||||
throw new ChaincodeException("Failed to transfer");
|
||||
}
|
||||
|
||||
// Decrease the allowance
|
||||
long updatedAllowance = currentAllowance - valueInt;
|
||||
stub.putStringState(allowanceKey.toString().trim(), String.valueOf(updatedAllowance));
|
||||
System.out.printf("spender %s allowance updated from %d to %d", spender, currentAllowance, updatedAllowance);
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("from", from);
|
||||
obj.put("to", to);
|
||||
obj.put("value", valueInt);
|
||||
stub.setEvent("Transfer", this.serialize(obj));
|
||||
System.out.println("transferFrom ended successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Transaction()
|
||||
private boolean doTransfer(final Context ctx, final String _from, final String _to, long _value) {
|
||||
|
||||
if (_from.equalsIgnoreCase(_to)) {
|
||||
throw new ChaincodeException("cannot transfer to and from same client account");
|
||||
}
|
||||
|
||||
if (_value < 0) { // transfer of 0 is allowed in ERC20, so just validate against negative amounts
|
||||
throw new ChaincodeException("transfer amount cannot be negative");
|
||||
}
|
||||
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
// Retrieve the current balance of the sender
|
||||
CompositeKey fromBalanceKey = stub.createCompositeKey(balancePrefix, _from.trim());
|
||||
String fromCurrentBalance = stub.getStringState(fromBalanceKey.toString().trim());
|
||||
if (fromCurrentBalance.isBlank() || fromCurrentBalance.length() == 0) {
|
||||
String errorMessage = String.format("client account %s has no balance", _from);
|
||||
throw new ChaincodeException(errorMessage);
|
||||
|
||||
}
|
||||
|
||||
long _fromCurrentBalance = Long.parseLong(fromCurrentBalance.toString().trim());
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Retrieve the current balance of the recepient
|
||||
CompositeKey toBalanceKey = stub.createCompositeKey(balancePrefix, _to);
|
||||
String toCurrentBalance = stub.getStringState(toBalanceKey.toString().trim());
|
||||
|
||||
long _toCurrentBalance = 0;
|
||||
// If recipient current balance doesn't yet exist, we'll create it with a
|
||||
// current balance of 0
|
||||
if (toCurrentBalance.isBlank() || toCurrentBalance.length() == 0) {
|
||||
_toCurrentBalance = 0;
|
||||
} else {
|
||||
_toCurrentBalance = Long.parseLong(toCurrentBalance.trim());
|
||||
}
|
||||
|
||||
// Update the balance
|
||||
long fromUpdatedBalance = _fromCurrentBalance - _value;
|
||||
long toUpdatedBalance = _toCurrentBalance + _value;
|
||||
|
||||
stub.putStringState(fromBalanceKey.toString().trim(), String.valueOf(fromUpdatedBalance));
|
||||
|
||||
stub.putStringState(toBalanceKey.toString().trim(), String.valueOf(toUpdatedBalance));
|
||||
|
||||
System.out.printf("client %s balance updated from %d to %d", _from, _fromCurrentBalance, fromUpdatedBalance);
|
||||
System.out.printf("recipient %s balance updated from %d to %d", _to, _toCurrentBalance, toUpdatedBalance);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows `spender` to spend `value` amount of tokens from the owner.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} spender The spender
|
||||
* @param {Integer} value The amount of tokens to be approved for transfer
|
||||
* @returns {Boolean} Return whether the approval was successful or not
|
||||
*/
|
||||
@Transaction()
|
||||
public boolean approve(final Context ctx, final String spender, final String value) {
|
||||
|
||||
String owner = ctx.getClientIdentity().getId();
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
CompositeKey allowanceKey = stub.createCompositeKey(allowancePrefix, owner, spender);
|
||||
long valueInt = Long.parseLong(value);
|
||||
stub.putStringState(allowanceKey.toString().trim(), String.valueOf(valueInt));
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("owner", owner);
|
||||
obj.put("spender", spender);
|
||||
obj.put("value", valueInt);
|
||||
stub.setEvent("Approval", this.serialize(obj));
|
||||
System.out.println("Approve ended successfully");
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of tokens which `spender` is allowed to withdraw from
|
||||
* `owner`.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} owner The owner of tokens
|
||||
* @param {String} spender The spender who are able to transfer the tokens
|
||||
* @returns {Number} Return the amount of remaining tokens allowed to spent
|
||||
*/
|
||||
|
||||
@Transaction()
|
||||
public long allowance(final Context ctx, final String owner, final String spender) {
|
||||
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
|
||||
CompositeKey allowanceKey = stub.createCompositeKey(allowancePrefix, owner, spender);
|
||||
String allowanceBytes = stub.getStringState(allowanceKey.toString().trim());
|
||||
|
||||
if (allowanceBytes.isBlank() || allowanceBytes.length() == 0) {
|
||||
|
||||
String errorMessage = String.format("spender account %s has no allowance from", spender, owner);
|
||||
throw new ChaincodeException(errorMessage);
|
||||
}
|
||||
|
||||
long allowance = Long.parseLong(allowanceBytes.toString().trim());
|
||||
return allowance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set optional infomation for a token.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} name The name of the token
|
||||
* @param {String} symbol The symbol of the token
|
||||
* @param {String} decimals The decimals of the token
|
||||
* @param {String} totalSupply The totalSupply of the token
|
||||
*/
|
||||
@Transaction()
|
||||
public boolean setOptions(final Context ctx, final String name, final String symbol, final String decimals) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
stub.putStringState(nameKey, name);
|
||||
stub.putStringState(symbolKey, symbol);
|
||||
stub.putStringState(decimalsKey, decimals);
|
||||
|
||||
System.out.printf("name:%s, symbol: %s, decimals: %s", name, symbol, decimals);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint creates new tokens and adds them to minter's account balance
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {Integer} amount amount of tokens to be minted
|
||||
* @returns {Object} The balance
|
||||
*/
|
||||
@Transaction()
|
||||
public boolean mint(final Context ctx, final String amount) {
|
||||
|
||||
// Check minter authorization - this sample assumes Org1 is the central banker
|
||||
// with privilege to mint new tokens
|
||||
|
||||
String clientMSPID = ctx.getClientIdentity().getMSPID();
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
if (!clientMSPID.equalsIgnoreCase("Org1MSP")) {
|
||||
throw new ChaincodeException("client is not authorized to mint new tokens");
|
||||
}
|
||||
|
||||
// Get ID of submitting client identity
|
||||
String minter = ctx.getClientIdentity().getId();
|
||||
long amountInt = Long.parseLong(amount.trim());
|
||||
if (amountInt <= 0) {
|
||||
throw new ChaincodeException("mint amount must be a positive integer");
|
||||
}
|
||||
|
||||
CompositeKey balanceKey = stub.createCompositeKey(balancePrefix, minter);
|
||||
|
||||
String currentBalanceBytes = stub.getStringState(balanceKey.toString().trim());
|
||||
// If minter current balance doesn't yet exist, we'll create it with a current
|
||||
// balance of 0
|
||||
long currentBalance = 0;
|
||||
|
||||
if (currentBalanceBytes.isBlank() || currentBalanceBytes.length() == 0) {
|
||||
|
||||
currentBalance = 0;
|
||||
|
||||
} else {
|
||||
|
||||
currentBalance = Long.parseLong(currentBalanceBytes.toString());
|
||||
|
||||
}
|
||||
long updatedBalance = currentBalance + amountInt;
|
||||
|
||||
stub.putStringState(balanceKey.toString().trim(), String.valueOf(updatedBalance));
|
||||
|
||||
// Increase totalSupply
|
||||
String totalSupplyBytes = stub.getStringState(totalSupplyKey.trim());
|
||||
long totalSupply = 0;
|
||||
if (totalSupplyBytes.isBlank() || totalSupplyBytes.length() == 0) {
|
||||
System.out.println("Initialize the tokenSupply");
|
||||
totalSupply = 0;
|
||||
|
||||
} else {
|
||||
|
||||
totalSupply = Long.parseLong(totalSupplyBytes.toString());
|
||||
}
|
||||
|
||||
totalSupply = totalSupply + amountInt;
|
||||
stub.putStringState(totalSupplyKey.trim(), String.valueOf(totalSupply));
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("from", "0x0");
|
||||
obj.put("to", minter);
|
||||
obj.put("value", amountInt);
|
||||
stub.setEvent("Transfer", this.serialize(obj));
|
||||
|
||||
// System.out.printf("minter account %s balance updated from %d to %d",minter,
|
||||
// currentBalance ,updatedBalance);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Burn redeem tokens from minter's account balance
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {Integer} amount amount of tokens to be burned
|
||||
* @returns {Object} The balance
|
||||
*/
|
||||
@Transaction()
|
||||
public boolean burn(final Context ctx, final String amount) {
|
||||
|
||||
// Check minter authorization - this sample assumes Org1 is the central banker
|
||||
// with privilege to burn tokens
|
||||
String clientMSPID = ctx.getClientIdentity().getMSPID();
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
if (!clientMSPID.equalsIgnoreCase("Org1MSP")) {
|
||||
throw new ChaincodeException("client is not authorized to mint new tokens");
|
||||
}
|
||||
|
||||
String minter = ctx.getClientIdentity().getId();
|
||||
|
||||
long amountInt = Long.parseLong(amount);
|
||||
|
||||
CompositeKey balanceKey = stub.createCompositeKey(balancePrefix, minter);
|
||||
|
||||
String currentBalanceBytes = stub.getStringState(balanceKey.toString().trim());
|
||||
if (currentBalanceBytes.isBlank() || currentBalanceBytes.length() == 0) {
|
||||
throw new ChaincodeException("The balance does not exist");
|
||||
}
|
||||
long currentBalance = Long.valueOf(currentBalanceBytes.toString());
|
||||
long updatedBalance = currentBalance - amountInt;
|
||||
|
||||
stub.putStringState(balanceKey.toString().trim(), String.valueOf(updatedBalance));
|
||||
|
||||
// Decrease totalSupply
|
||||
String totalSupplyBytes = stub.getStringState(totalSupplyKey.toString().trim());
|
||||
if (totalSupplyBytes.isBlank() || totalSupplyBytes.length() == 0) {
|
||||
throw new ChaincodeException("totalSupply does not exist.");
|
||||
}
|
||||
long totalSupply = Long.parseLong(totalSupplyBytes.toString()) - amountInt;
|
||||
stub.putStringState(totalSupplyKey.toString().trim(), String.valueOf(totalSupply));
|
||||
|
||||
// Emit the Transfer event
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("from", minter);
|
||||
obj.put("to", "0x0");
|
||||
obj.put("value", amountInt);
|
||||
stub.setEvent("Transfer", this.serialize(obj));
|
||||
System.out.printf("minter account %s balance updated from %d to %d", minter, currentBalance, updatedBalance);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* ClientAccountBalance returns the balance of the requesting client's account.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {Number} Returns the account balance
|
||||
*/
|
||||
|
||||
@Transaction()
|
||||
public long getClientAccountBalance(final Context ctx) {
|
||||
// Get ID of submitting client identity
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
String clientAccountID = ctx.getClientIdentity().getId();
|
||||
CompositeKey balanceKey = stub.createCompositeKey(balancePrefix, clientAccountID);
|
||||
String balanceBytes = stub.getStringState(balanceKey.toString().trim());
|
||||
if (balanceBytes.isBlank() || balanceBytes.length() == 0) {
|
||||
|
||||
String errorMessage = String.format("the account %s does not exist", clientAccountID);
|
||||
throw new ChaincodeException(errorMessage);
|
||||
}
|
||||
long balance = Long.parseLong(balanceBytes.trim());
|
||||
|
||||
return balance;
|
||||
}
|
||||
|
||||
// ClientAccountID returns the id of the requesting client's account.
|
||||
// In this implementation, the client account ID is the clientId itself.
|
||||
// Users can use this function to get their own account id, which they can then
|
||||
// give to others as the payment address
|
||||
@Transaction()
|
||||
public String getClientAccountID(final Context ctx) {
|
||||
// Get ID of submitting client identity
|
||||
String clientAccountID = ctx.getClientIdentity().getId();
|
||||
return clientAccountID;
|
||||
}
|
||||
|
||||
private byte[] serialize(Object object) {
|
||||
String jsonStr = new JSONObject(object).toString();
|
||||
return jsonStr.getBytes(UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
package org.example;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
//import org.hyperledger.fabric.samples.fabcar.TestUtil;
|
||||
|
||||
import org.hyperledger.fabric.protos.msp.Identities.SerializedIdentity;
|
||||
import org.hyperledger.fabric.protos.peer.ChaincodeEventPackage;
|
||||
import org.hyperledger.fabric.protos.peer.ProposalPackage;
|
||||
import org.hyperledger.fabric.shim.Chaincode;
|
||||
import org.hyperledger.fabric.shim.ChaincodeStub;
|
||||
import org.hyperledger.fabric.shim.ledger.CompositeKey;
|
||||
import org.hyperledger.fabric.shim.ledger.KeyModification;
|
||||
import org.hyperledger.fabric.shim.ledger.KeyValue;
|
||||
import org.hyperledger.fabric.shim.ledger.QueryResultsIterator;
|
||||
import org.hyperledger.fabric.shim.ledger.QueryResultsIteratorWithMetadata;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
public final class ChaincodeStubNaiveImpl implements ChaincodeStub {
|
||||
private List<String> args;
|
||||
private List<byte[]> argsAsByte;
|
||||
private final Map<String, ByteString> state;
|
||||
private final Chaincode.Response resp;
|
||||
|
||||
public String certificate = "MIICXTCCAgSgAwIBAgIUeLy6uQnq8wwyElU/jCKRYz3tJiQwCgYIKoZIzj0EAwIw"
|
||||
+ "eTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh" + "biBGcmFuY2lzY28xGTAXBgNVBAoTEEludGVybmV0IFdpZGdldHMxDDAKBgNVBAsT"
|
||||
+ "A1dXVzEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTcwOTA4MDAxNTAwWhcNMTgw" + "OTA4MDAxNTAwWjBdMQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xp"
|
||||
+ "bmExFDASBgNVBAoTC0h5cGVybGVkZ2VyMQ8wDQYDVQQLEwZGYWJyaWMxDjAMBgNV" + "BAMTBWFkbWluMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFq/90YMuH4tWugHa"
|
||||
+ "oyZtt4Mbwgv6CkBSDfYulVO1CVInw1i/k16DocQ/KSDTeTfgJxrX1Ree1tjpaodG" + "1wWyM6OBhTCBgjAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4E"
|
||||
+ "FgQUhKs/VJ9IWJd+wer6sgsgtZmxZNwwHwYDVR0jBBgwFoAUIUd4i/sLTwYWvpVr" + "TApzcT8zv/kwIgYDVR0RBBswGYIXQW5pbHMtTWFjQm9vay1Qcm8ubG9jYWwwCgYI"
|
||||
+ "KoZIzj0EAwIDRwAwRAIgCoXaCdU8ZiRKkai0QiXJM/GL5fysLnmG2oZ6XOIdwtsC" + "IEmCsI8Mhrvx1doTbEOm7kmIrhQwUVDBNXCWX1t3kJVN";
|
||||
|
||||
public static final String CERT_WITH_ATTRS = "MIIB6TCCAY+gAwIBAgIUHkmY6fRP0ANTvzaBwKCkMZZPUnUwCgYIKoZIzj0EAwIw"
|
||||
+ "GzEZMBcGA1UEAxMQZmFicmljLWNhLXNlcnZlcjAeFw0xNzA5MDgwMzQyMDBaFw0x" + "ODA5MDgwMzQyMDBaMB4xHDAaBgNVBAMTE015VGVzdFVzZXJXaXRoQXR0cnMwWTAT"
|
||||
+ "BgcqhkjOPQIBBggqhkjOPQMBBwNCAATmB1r3CdWvOOP3opB3DjJnW3CnN8q1ydiR" + "dzmuA6A2rXKzPIltHvYbbSqISZJubsy8gVL6GYgYXNdu69RzzFF5o4GtMIGqMA4G"
|
||||
+ "A1UdDwEB/wQEAwICBDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTYKLTAvJJK08OM" + "VGwIhjMQpo2DrjAfBgNVHSMEGDAWgBTEs/52DeLePPx1+65VhgTwu3/2ATAiBgNV"
|
||||
+ "HREEGzAZghdBbmlscy1NYWNCb29rLVByby5sb2NhbDAmBggqAwQFBgcIAQQaeyJh" + "dHRycyI6eyJhdHRyMSI6InZhbDEifX0wCgYIKoZIzj0EAwIDSAAwRQIhAPuEqWUp"
|
||||
+ "svTTvBqLR5JeQSctJuz3zaqGRqSs2iW+QB3FAiAIP0mGWKcgSGRMMBvaqaLytBYo" + "9v3hRt1r8j8vN0pMcg==";
|
||||
|
||||
public static final String CERT_WITH_DNS = "MIICGjCCAcCgAwIBAgIRAIPRwJHVLhHK47XK0BbFZJswCgYIKoZIzj0EAwIwczEL"
|
||||
+ "MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG" + "cmFuY2lzY28xGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh"
|
||||
+ "Lm9yZzIuZXhhbXBsZS5jb20wHhcNMTcwNjIzMTIzMzE5WhcNMjcwNjIxMTIzMzE5" + "WjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN"
|
||||
+ "U2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWVXNlcjFAb3JnMi5leGFtcGxlLmNvbTBZ" + "MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBd9SsEiFH1/JIb3qMEPLR2dygokFVKW"
|
||||
+ "eINcB0Ni4TBRkfIWWUJeCANTUY11Pm/+5gs+fBTqBz8M2UzpJDVX7+2jTTBLMA4G" + "A1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIKfUfvpGproH"
|
||||
+ "cwyFD+0sE3XfJzYNcif0jNwvgOUFZ4AFMAoGCCqGSM49BAMCA0gAMEUCIQC8NIMw" + "e4ym/QRwCJb5umbONNLSVQuEpnPsJrM/ssBPvgIgQpe2oYa3yO3USro9nBHjpM3L"
|
||||
+ "KsFQrpVnF8O6hoHOYZQ=";
|
||||
|
||||
public static final String CERT_MULTIPLE_ATTRIBUTES = "MIIChzCCAi6gAwIBAgIURilAHeqwLu/fNUv8eZoGPRh3H4IwCgYIKoZIzj0EAwIw"
|
||||
+ "czELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh" + "biBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT"
|
||||
+ "E2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTkwNzMxMTYxNzAwWhcNMjAwNzMwMTYy" + "MjAwWjAgMQ8wDQYDVQQLEwZjbGllbnQxDTALBgNVBAMTBHRlc3QwWTATBgcqhkjO"
|
||||
+ "PQIBBggqhkjOPQMBBwNCAAR2taQK8w7D3hr3gBxCz+8eV4KSv7pFQfNjDHMMe9J9" + "LJwcLpVTT5hYiLLRaqQonLBxBE3Ey0FneySvFuBScas3o4HyMIHvMA4GA1UdDwEB"
|
||||
+ "/wQEAwIHgDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQi3mhXS/WzcjBniwAmPdYP" + "kHqVVzArBgNVHSMEJDAigCC7VXjmSEugjAB/A0S6vfMxLsUIgag9WVNwtwwebnRC"
|
||||
+ "7TCBggYIKgMEBQYHCAEEdnsiYXR0cnMiOnsiYXR0cjEiOiJ2YWwxIiwiZm9vIjoi" + "YmFyIiwiaGVsbG8iOiJ3b3JsZCIsImhmLkFmZmlsaWF0aW9uIjoiIiwiaGYuRW5y"
|
||||
+ "b2xsbWVudElEIjoidGVzdCIsImhmLlR5cGUiOiJjbGllbnQifX0wCgYIKoZIzj0E" + "AwIDRwAwRAIgQxEFvnZTEsf3CSZmp9IYsxcnEOtVYleOd86LAKtk1wICIH7XOPwW"
|
||||
+ "/RE4Z8WLZzFei/78Oezbx6obOvBxPMsVWRe5";
|
||||
|
||||
public ChaincodeStubNaiveImpl() {
|
||||
|
||||
args = new ArrayList<>();
|
||||
args.add("func1");
|
||||
args.add("param1");
|
||||
args.add("param2");
|
||||
state = new HashMap<>();
|
||||
state.put("a", ByteString.copyFrom("asdf", StandardCharsets.UTF_8));
|
||||
argsAsByte = null;
|
||||
resp = new Chaincode.Response(404, "Wrong cc name", new byte[] {});
|
||||
|
||||
}
|
||||
|
||||
ChaincodeStubNaiveImpl(final List<String> args) {
|
||||
this.args = args;
|
||||
state = new HashMap<>();
|
||||
state.put("a", ByteString.copyFrom("asdf", StandardCharsets.UTF_8));
|
||||
|
||||
argsAsByte = null;
|
||||
|
||||
resp = new Chaincode.Response(404, "Wrong cc name", new byte[] {});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<byte[]> getArgs() {
|
||||
if (argsAsByte == null) {
|
||||
argsAsByte = args.stream().map(i -> i.getBytes()).collect(Collectors.toList());
|
||||
}
|
||||
return argsAsByte;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStringArgs() {
|
||||
return args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFunction() {
|
||||
return args.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getParameters() {
|
||||
return args.subList(1, args.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTxId() {
|
||||
return "tx0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannelId() {
|
||||
return "ch0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Chaincode.Response invokeChaincode(final String chaincodeName, final List<byte[]> args, final String channel) {
|
||||
return resp;
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void putStringState(final String key, final String value) {
|
||||
putState(key, value.getBytes(UTF_8));
|
||||
}
|
||||
|
||||
|
||||
public String getStringState(final String key) {
|
||||
//return new String(getState(key), UTF_8);
|
||||
if(state.get(key) == null)
|
||||
return "";
|
||||
else
|
||||
return new String(getState(key), UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getState(final String key) {
|
||||
return state.get(key).toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStateValidationParameter(final String key) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putState(final String key, final byte[] value) {
|
||||
state.put(key, ByteString.copyFrom(value));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateValidationParameter(final String key, final byte[] value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delState(final String key) {
|
||||
state.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getStateByRange(final String startKey, final String endKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIteratorWithMetadata<KeyValue> getStateByRangeWithPagination(final String startKey, final String endKey, final int pageSize,
|
||||
final String bookmark) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getStateByPartialCompositeKey(final String compositeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getStateByPartialCompositeKey(final String objectType, final String... attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getStateByPartialCompositeKey(final CompositeKey compositeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIteratorWithMetadata<KeyValue> getStateByPartialCompositeKeyWithPagination(final CompositeKey compositeKey, final int pageSize,
|
||||
final String bookmark) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompositeKey createCompositeKey(final String objectType, final String... attributes) {
|
||||
final CompositeKey key = new CompositeKey(objectType, attributes);
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompositeKey splitCompositeKey(final String compositeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getQueryResult(final String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIteratorWithMetadata<KeyValue> getQueryResultWithPagination(final String query, final int pageSize, final String bookmark) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyModification> getHistoryForKey(final String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPrivateData(final String collection, final String key) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPrivateDataHash(final String collection, final String key) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPrivateDataValidationParameter(final String collection, final String key) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putPrivateData(final String collection, final String key, final byte[] value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrivateDataValidationParameter(final String collection, final String key, final byte[] value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delPrivateData(final String collection, final String key) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getPrivateDataByRange(final String collection, final String startKey, final String endKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getPrivateDataByPartialCompositeKey(final String collection, final String compositeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getPrivateDataByPartialCompositeKey(final String collection, final CompositeKey compositeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getPrivateDataByPartialCompositeKey(final String collection, final String objectType, final String... attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResultsIterator<KeyValue> getPrivateDataQueryResult(final String collection, final String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEvent(final String name, final byte[] payload) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChaincodeEventPackage.ChaincodeEvent getEvent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProposalPackage.SignedProposal getSignedProposal() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getTxTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCreator() {
|
||||
return buildSerializedIdentity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, byte[]> getTransient() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBinding() {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
void setStringArgs(final List<String> args) {
|
||||
this.args = args;
|
||||
this.argsAsByte = args.stream().map(i -> i.getBytes()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public byte[] buildSerializedIdentity() {
|
||||
final SerializedIdentity.Builder identity = SerializedIdentity.newBuilder();
|
||||
identity.setMspid("Org1MSP");
|
||||
final byte[] decodedCert = Base64.getDecoder().decode(this.certificate);
|
||||
identity.setIdBytes(ByteString.copyFrom(decodedCert));
|
||||
final SerializedIdentity builtIdentity = identity.build();
|
||||
return builtIdentity.toByteArray();
|
||||
}
|
||||
|
||||
// Used by tests to control which serialized identity is returned by
|
||||
// buildSerializedIdentity
|
||||
public void setCertificate(final String certificateToTest) {
|
||||
this.certificate = certificateToTest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMspId() {
|
||||
return "Org1MSP";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.example;
|
||||
|
||||
|
||||
import org.hyperledger.fabric.contract.Context;
|
||||
|
||||
import org.hyperledger.fabric.contract.ClientIdentity;
|
||||
|
||||
import org.example.TokenERC20Contract;
|
||||
import org.hyperledger.fabric.shim.ChaincodeStub;
|
||||
import org.hyperledger.fabric.shim.ledger.CompositeKey;
|
||||
import org.hyperledger.fabric.shim.ChaincodeException;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TokenERC20ContractTest {
|
||||
|
||||
final private String balancePrefix = "balance";
|
||||
|
||||
final private String nameKey = "name";
|
||||
final private String symbolKey = "symbol";
|
||||
final private String decimalsKey = "decimals";
|
||||
final private String totalSupplyKey = "totalSupply";
|
||||
|
||||
@Nested
|
||||
class InvokeQueryERC20TokenOPtionsTransaction {
|
||||
|
||||
@Test
|
||||
public void whenTokenNameExists() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(nameKey)).thenReturn("ARBTToken");
|
||||
|
||||
String toknName = contract.tokenName(ctx);
|
||||
|
||||
assertThat(toknName).isEqualTo("ARBTToken");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTokenNameDoesNotExist() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(nameKey)).thenReturn("");
|
||||
|
||||
Throwable thrown = catchThrowable(() -> {
|
||||
contract.tokenName(ctx);
|
||||
});
|
||||
|
||||
assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
|
||||
.hasMessage("Sorry ! Token name not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTokenSymbolExists() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(symbolKey)).thenReturn("ARBT");
|
||||
|
||||
String toknName = contract.tokenSymbol(ctx);
|
||||
|
||||
assertThat(toknName).isEqualTo("ARBT");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTokenSymbolDoesNotExist() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(symbolKey)).thenReturn("");
|
||||
|
||||
Throwable thrown = catchThrowable(() -> {
|
||||
contract.tokenSymbol(ctx);
|
||||
});
|
||||
|
||||
assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
|
||||
.hasMessage("Sorry ! Token symbol not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTokenDecimalExists() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(decimalsKey)).thenReturn("18");
|
||||
|
||||
long decimal = contract.decimals(ctx);
|
||||
|
||||
assertThat(decimal).isEqualTo(18);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTokenDecimalNotExists() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(decimalsKey)).thenReturn("");
|
||||
|
||||
Throwable thrown = catchThrowable(() -> {
|
||||
contract.decimals(ctx);
|
||||
});
|
||||
|
||||
assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
|
||||
.hasMessage("Sorry ! Decimal not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTokenTotalSupplyExists() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(totalSupplyKey)).thenReturn("222222222222");
|
||||
|
||||
long totalSupply = contract.totalSupply(ctx);
|
||||
|
||||
assertThat(totalSupply).isEqualTo(222222222222L);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTokenTotalSupplyNotExists() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
when(stub.getStringState(totalSupplyKey)).thenReturn("");
|
||||
|
||||
Throwable thrown = catchThrowable(() -> {
|
||||
contract.totalSupply(ctx);
|
||||
});
|
||||
|
||||
assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
|
||||
.hasMessage("Sorry ! Total Supply not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenClientAccountIDTest() throws Exception {
|
||||
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
final ChaincodeStub stub = new ChaincodeStubNaiveImpl();
|
||||
final ClientIdentity identity = new ClientIdentity(stub);
|
||||
assertThat(identity.getMSPID()).isEqualTo("Org1MSP");
|
||||
when(ctx.getClientIdentity()).thenReturn(identity);
|
||||
String id = contract.getClientAccountID(ctx);
|
||||
String actualId = "x509::CN=admin, OU=Fabric, O=Hyperledger, ST=North Carolina, C=US::CN=example.com,"
|
||||
+ " OU=WWW, O=Internet Widgets, L=San Francisco, ST=California, C=US";
|
||||
assertThat(id).isEqualTo(actualId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TokenOperationsInvoke {
|
||||
|
||||
@Test
|
||||
public void setOptionsTest() {
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
ChaincodeStub stub = mock(ChaincodeStub.class);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
contract.setOptions(ctx, "ARBTToken", "ARBT", "18");
|
||||
verify(stub).putStringState(nameKey, "ARBTToken");
|
||||
verify(stub).putStringState(symbolKey, "ARBT");
|
||||
verify(stub).putStringState(decimalsKey, "18");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenOrgMintTokensTest() throws Exception {
|
||||
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
final ChaincodeStub stub = new ChaincodeStubNaiveImpl();
|
||||
final ClientIdentity identity = new ClientIdentity(stub);
|
||||
when(ctx.getClientIdentity()).thenReturn(identity);
|
||||
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
boolean returnValue = contract.mint(ctx, "1000");
|
||||
assertThat(returnValue).isEqualTo(true);
|
||||
String totalSupply = stub.getStringState(totalSupplyKey.trim());
|
||||
assertThat(totalSupply).isEqualTo("1000");
|
||||
String minter = ctx.getClientIdentity().getId();
|
||||
CompositeKey balanceKey = stub.createCompositeKey(balancePrefix, minter);
|
||||
String updatedBalance = stub.getStringState(balanceKey.toString().trim());
|
||||
assertThat(updatedBalance).isEqualTo("1000");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUserTransferTokenTest() throws Exception {
|
||||
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
final ChaincodeStub stub = new ChaincodeStubNaiveImpl();
|
||||
final ClientIdentity identity = new ClientIdentity(stub);
|
||||
when(ctx.getClientIdentity()).thenReturn(identity);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
boolean returnValue = contract.mint(ctx, "1000");
|
||||
String minter = ctx.getClientIdentity().getId();
|
||||
String _to = "x509::CN=User1@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";
|
||||
boolean transferResult = contract.transfer(ctx, _to, "100");
|
||||
CompositeKey toBalanceKey = stub.createCompositeKey(balancePrefix, _to);
|
||||
String _toCurrentBalance = stub.getStringState(toBalanceKey.toString().trim());
|
||||
Long totalSupply = contract.totalSupply(ctx);
|
||||
Long fromBalance = contract.balanceOf(ctx, minter);
|
||||
((ChaincodeStubNaiveImpl) stub).setCertificate(ChaincodeStubNaiveImpl.CERT_WITH_DNS);
|
||||
Long _toBalance = contract.balanceOf(ctx, _to);
|
||||
|
||||
assertThat(transferResult).isEqualTo(true);
|
||||
assertThat(returnValue).isEqualTo(true);
|
||||
assertThat(totalSupply).isEqualTo(1000);
|
||||
assertThat(_toCurrentBalance).isEqualTo("100");
|
||||
assertThat(fromBalance).isEqualTo(900);
|
||||
assertThat(_toBalance).isEqualTo(100);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenOrgBurnsTokenTest() throws Exception {
|
||||
|
||||
TokenERC20Contract contract = new TokenERC20Contract();
|
||||
Context ctx = mock(Context.class);
|
||||
final ChaincodeStub stub = new ChaincodeStubNaiveImpl();
|
||||
final ClientIdentity identity = new ClientIdentity(stub);
|
||||
when(ctx.getClientIdentity()).thenReturn(identity);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
boolean returnValue = contract.mint(ctx, "1000");
|
||||
String minter = ctx.getClientIdentity().getId();
|
||||
boolean burnResult = contract.burn(ctx, "100");
|
||||
Long totalSupply = contract.totalSupply(ctx);
|
||||
Long fromBalance = contract.balanceOf(ctx, minter);
|
||||
assertThat(returnValue).isEqualTo(true);
|
||||
assertThat(burnResult).isEqualTo(true);
|
||||
assertThat(totalSupply).isEqualTo(900);
|
||||
assertThat(fromBalance).isEqualTo(900);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class InvokeERC20AllowanceTransactions {
|
||||
|
||||
private Context ctx = null;
|
||||
private ChaincodeStub stub = null;
|
||||
private ClientIdentity identity = null;
|
||||
private TokenERC20Contract contract = null;
|
||||
|
||||
@BeforeEach
|
||||
public void initialize() {
|
||||
try {
|
||||
|
||||
this.ctx = mock(Context.class);
|
||||
this.stub = new ChaincodeStubNaiveImpl();
|
||||
this.identity = new ClientIdentity(stub);
|
||||
when(ctx.getClientIdentity()).thenReturn(identity);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
contract = new TokenERC20Contract();
|
||||
contract.mint(ctx, "1000");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void approveForTokenAllowanceTest() {
|
||||
|
||||
String spender = "x509::CN=User1@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";
|
||||
boolean result = contract.approve(ctx, spender, "200");
|
||||
assertThat(result).isEqualTo(true);
|
||||
String owner = ctx.getClientIdentity().getId();
|
||||
long allowance = contract.allowance(ctx, owner, spender);
|
||||
assertThat(allowance).isEqualTo(200);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowanceTransferFromTest() throws Exception {
|
||||
|
||||
/*
|
||||
* ChaincodeStub localStub = new ChaincodeStubNaiveImpl();
|
||||
* ((ChaincodeStubNaiveImpl)
|
||||
* localStub).setCertificate(ChaincodeStubNaiveImpl.CERT_WITH_DNS); Context
|
||||
* localCtx = mock(Context.class); ClientIdentity localidentity = new
|
||||
* ClientIdentity(localStub);
|
||||
* when(localCtx.getClientIdentity()).thenReturn(localidentity);
|
||||
* when(localCtx.getStub()).thenReturn(localStub);
|
||||
*/
|
||||
|
||||
String spender = "x509::CN=User1@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";
|
||||
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";
|
||||
boolean result = contract.approve(ctx, spender, "200");
|
||||
String owner = ctx.getClientIdentity().getId();
|
||||
|
||||
((ChaincodeStubNaiveImpl) stub).setCertificate(ChaincodeStubNaiveImpl.CERT_WITH_DNS);
|
||||
identity = new ClientIdentity(stub);
|
||||
when(ctx.getClientIdentity()).thenReturn(identity);
|
||||
when(ctx.getStub()).thenReturn(stub);
|
||||
boolean transferResult = contract.transferFrom(ctx, owner, to, "100");
|
||||
long allowance = contract.allowance(ctx, owner, spender);
|
||||
|
||||
assertThat(result).isEqualTo(true);
|
||||
assertThat(transferResult).isEqualTo(true);
|
||||
assertThat(allowance).isEqualTo(100);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue