From 590851471b25ab1a6d54b15a6e553802e2e6e65c Mon Sep 17 00:00:00 2001 From: "renjithkn@gmail.com" Date: Mon, 24 Jan 2022 16:50:18 +0000 Subject: [PATCH] ERC20 Java chaincode implementation exmaple. Signed-off-by: renjithkn@gmail.com --- token-erc-20/chaincode-java/pom.xml | 165 ++++++ .../java/org/example/TokenERC20Contract.java | 519 ++++++++++++++++++ .../org/example/ChaincodeStubNaiveImpl.java | 337 ++++++++++++ .../org/example/TokenERC20ContractTest.java | 341 ++++++++++++ 4 files changed, 1362 insertions(+) create mode 100644 token-erc-20/chaincode-java/pom.xml create mode 100644 token-erc-20/chaincode-java/src/main/java/org/example/TokenERC20Contract.java create mode 100644 token-erc-20/chaincode-java/src/test/org/example/ChaincodeStubNaiveImpl.java create mode 100644 token-erc-20/chaincode-java/src/test/org/example/TokenERC20ContractTest.java diff --git a/token-erc-20/chaincode-java/pom.xml b/token-erc-20/chaincode-java/pom.xml new file mode 100644 index 00000000..ff00fc1d --- /dev/null +++ b/token-erc-20/chaincode-java/pom.xml @@ -0,0 +1,165 @@ + + 4.0.0 + TokenERC20Contract + TokenERC20Contract + 1.0-SNAPSHOT + + + + 1.8 + UTF-8 + UTF-8 + + + 2.2.0 + + + 1.0.13 + 1.7.5 + + + 5.4.2 + 1.3.0-RC1 + + + + + + jitpack.io + https://www.jitpack.io + + + artifactory + https://hyperledger.jfrog.io/hyperledger/fabric-maven + + + + + + + + org.hyperledger.fabric-chaincode-java + fabric-chaincode-shim + ${fabric-chaincode-java.version} + compile + + + + org.hyperledger.fabric-chaincode-java + fabric-chaincode-protos + ${fabric-chaincode-java.version} + compile + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + compile + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + 3.11.1 + + + + + org.mockito + mockito-core + 2.23.0 + + + + + + org.json + json + 20180813 + + + + + src + + + + maven-surefire-plugin + 2.22.0 + + + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + chaincode + + + org.hyperledger.fabric.contract.ContractRouter + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + diff --git a/token-erc-20/chaincode-java/src/main/java/org/example/TokenERC20Contract.java b/token-erc-20/chaincode-java/src/main/java/org/example/TokenERC20Contract.java new file mode 100644 index 00000000..20d23185 --- /dev/null +++ b/token-erc-20/chaincode-java/src/main/java/org/example/TokenERC20Contract.java @@ -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); + } + +} diff --git a/token-erc-20/chaincode-java/src/test/org/example/ChaincodeStubNaiveImpl.java b/token-erc-20/chaincode-java/src/test/org/example/ChaincodeStubNaiveImpl.java new file mode 100644 index 00000000..60f73d83 --- /dev/null +++ b/token-erc-20/chaincode-java/src/test/org/example/ChaincodeStubNaiveImpl.java @@ -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 args; + private List argsAsByte; + private final Map 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 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 getArgs() { + if (argsAsByte == null) { + argsAsByte = args.stream().map(i -> i.getBytes()).collect(Collectors.toList()); + } + return argsAsByte; + } + + @Override + public List getStringArgs() { + return args; + } + + @Override + public String getFunction() { + return args.get(0); + } + + @Override + public List 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 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 getStateByRange(final String startKey, final String endKey) { + return null; + } + + @Override + public QueryResultsIteratorWithMetadata getStateByRangeWithPagination(final String startKey, final String endKey, final int pageSize, + final String bookmark) { + return null; + } + + @Override + public QueryResultsIterator getStateByPartialCompositeKey(final String compositeKey) { + return null; + } + + @Override + public QueryResultsIterator getStateByPartialCompositeKey(final String objectType, final String... attributes) { + return null; + } + + @Override + public QueryResultsIterator getStateByPartialCompositeKey(final CompositeKey compositeKey) { + return null; + } + + @Override + public QueryResultsIteratorWithMetadata 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 getQueryResult(final String query) { + return null; + } + + @Override + public QueryResultsIteratorWithMetadata getQueryResultWithPagination(final String query, final int pageSize, final String bookmark) { + return null; + } + + @Override + public QueryResultsIterator 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 getPrivateDataByRange(final String collection, final String startKey, final String endKey) { + return null; + } + + @Override + public QueryResultsIterator getPrivateDataByPartialCompositeKey(final String collection, final String compositeKey) { + return null; + } + + @Override + public QueryResultsIterator getPrivateDataByPartialCompositeKey(final String collection, final CompositeKey compositeKey) { + return null; + } + + @Override + public QueryResultsIterator getPrivateDataByPartialCompositeKey(final String collection, final String objectType, final String... attributes) { + return null; + } + + @Override + public QueryResultsIterator 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 getTransient() { + return null; + } + + @Override + public byte[] getBinding() { + return new byte[0]; + } + + void setStringArgs(final List 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"; + } +} diff --git a/token-erc-20/chaincode-java/src/test/org/example/TokenERC20ContractTest.java b/token-erc-20/chaincode-java/src/test/org/example/TokenERC20ContractTest.java new file mode 100644 index 00000000..89392560 --- /dev/null +++ b/token-erc-20/chaincode-java/src/test/org/example/TokenERC20ContractTest.java @@ -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); + + } + + } + +}