ERC20 Java chaincode implementation exmaple.

Signed-off-by: renjithkn@gmail.com <renjithkn@gmail.com>
This commit is contained in:
renjithkn@gmail.com 2022-01-24 16:50:18 +00:00
parent 77431f5b39
commit 590851471b
4 changed files with 1362 additions and 0 deletions

View 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>

View file

@ -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);
}
}

View file

@ -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";
}
}

View file

@ -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);
}
}
}