mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-26 19:45:10 +00:00
464 lines
16 KiB
Java
464 lines
16 KiB
Java
/*
|
||
* 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;
|
||
|
||
import com.google.common.base.Strings;
|
||
|
||
@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 {
|
||
|
||
private static final String BALANCE_PREFIX = "balance";
|
||
private static final String ALLOWANCE_PREFIX = "allowance";
|
||
private static final String NAME_KEY = "name";
|
||
private static final String SYMBOL_KEY = "symbol";
|
||
private static final String DECIMALS_KEY = "decimals";
|
||
private static final String TOTAL_SUPPLY_KEY = "totalSupply";
|
||
private static final String TRANSFER_EVENT = "Transfer";
|
||
private static final String FROM = "from";
|
||
private static final String TO = "to";
|
||
private static final String VALUE= "value";
|
||
private static final String ERC20_OWNER_MSPID= "Org1MSP";
|
||
|
||
|
||
/**
|
||
* @Desc 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 ctx the transaction context
|
||
* @returns Returns the name of the token
|
||
*/
|
||
|
||
@Transaction()
|
||
public String tokenName(final Context ctx) {
|
||
|
||
String tokenName = ctx.getStub().getStringState(NAME_KEY);
|
||
|
||
if (Strings.isNullOrEmpty(tokenName)) {
|
||
|
||
throw new ChaincodeException("Sorry ! Token name not found.");
|
||
}
|
||
|
||
return tokenName;
|
||
|
||
}
|
||
|
||
/**
|
||
* @Desc Return the symbol of the token. E.g. “HIX<49><58>??.
|
||
* @param ctx the transaction context
|
||
* @returns Returns the symbol of the token
|
||
*/
|
||
@Transaction()
|
||
public String tokenSymbol(final Context ctx) {
|
||
String tokenSymbol = ctx.getStub().getStringState(SYMBOL_KEY);
|
||
if (Strings.isNullOrEmpty(tokenSymbol)) {
|
||
throw new ChaincodeException("Sorry ! Token symbol not found.");
|
||
}
|
||
|
||
return tokenSymbol;
|
||
|
||
}
|
||
|
||
/**
|
||
* @Desc 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 ctx the transaction context
|
||
* @returns Returns the number of decimals
|
||
*/
|
||
@Transaction()
|
||
public int decimals(final Context ctx) {
|
||
|
||
String decimals = ctx.getStub().getStringState(DECIMALS_KEY);
|
||
if (Strings.isNullOrEmpty(decimals)) {
|
||
|
||
throw new ChaincodeException("Sorry ! Decimal not found.");
|
||
}
|
||
return Integer.parseInt(decimals);
|
||
}
|
||
|
||
/**
|
||
* @Desc Return the total token supply.
|
||
* @param ctx the transaction context
|
||
* @returns Returns the total token supply
|
||
*/
|
||
@Transaction()
|
||
public long totalSupply(final Context ctx) {
|
||
|
||
String totalSupply = ctx.getStub().getStringState(TOTAL_SUPPLY_KEY);
|
||
if (Strings.isNullOrEmpty(totalSupply)) {
|
||
|
||
throw new ChaincodeException("Sorry ! Total Supply not found.");
|
||
}
|
||
return Long.parseLong(totalSupply);
|
||
}
|
||
|
||
/**
|
||
* BalanceOf returns the balance of the given account.
|
||
*
|
||
* @param ctx the transaction context
|
||
* @param owner The owner from which the balance will be retrieved
|
||
* @returns Returns the account balance
|
||
*/
|
||
@Transaction()
|
||
public long balanceOf(final Context ctx, final String owner) {
|
||
|
||
ChaincodeStub stub = ctx.getStub();
|
||
CompositeKey balanceKey = ctx.getStub().createCompositeKey(BALANCE_PREFIX, owner);
|
||
String balance = stub.getStringState(balanceKey.toString());
|
||
if (Strings.isNullOrEmpty(balance)) {
|
||
String errorMessage = String.format("Balance of the owner %s not exists", owner);
|
||
throw new ChaincodeException(errorMessage);
|
||
}
|
||
return Long.parseLong(balance);
|
||
|
||
}
|
||
|
||
/**
|
||
* @Desc Transfer transfers tokens from client account to recipient account. recipient account
|
||
* must be a valid clientID as returned by the ClientAccountID() function.
|
||
*
|
||
* @param ctx the transaction context
|
||
* @param to The recipient
|
||
* @param value The amount of token to be transferred
|
||
* @returns Return whether the transfer was successful or not
|
||
*/
|
||
@Transaction()
|
||
public void transfer(final Context ctx, final String to, long _value) {
|
||
|
||
String from = ctx.getClientIdentity().getId();
|
||
this.doTransfer(ctx, from, to, _value);
|
||
ctx.getStub().setEvent(TRANSFER_EVENT, new JSONObject().put(FROM, from).put(TO, to)
|
||
.put(VALUE, _value).toString().getBytes(UTF_8));
|
||
|
||
}
|
||
|
||
/**
|
||
* Transfer `value` amount of tokens from `from` to `to`.
|
||
*
|
||
* @param ctx the transaction context
|
||
* @param from The sender
|
||
* @param to The recipient
|
||
* @param value The amount of token to be transferred
|
||
* @returns Return whether the transfer was successful or not
|
||
*/
|
||
@Transaction()
|
||
public void 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(ALLOWANCE_PREFIX, from, spender);
|
||
String currentAllowanceStr = stub.getStringState(allowanceKey.toString());
|
||
if (Strings.isNullOrEmpty(currentAllowanceStr)) {
|
||
String errorMessage = String.format("Spender %s has no allowance from %s", spender, from);
|
||
throw new ChaincodeException(errorMessage);
|
||
}
|
||
long currentAllowance = Long.parseLong(currentAllowanceStr);
|
||
|
||
// 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.");
|
||
throw new ChaincodeException(errorMessage);
|
||
|
||
}
|
||
|
||
this.doTransfer(ctx, from, to, valueInt);
|
||
|
||
// Decrease the allowance
|
||
long updatedAllowance = currentAllowance - valueInt;
|
||
stub.putStringState(allowanceKey.toString(), String.valueOf(updatedAllowance));
|
||
stub.setEvent(TRANSFER_EVENT, new JSONObject().put(FROM, from).put(TO, to)
|
||
.put(VALUE, valueInt).toString().getBytes(UTF_8));
|
||
|
||
}
|
||
|
||
private void 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(BALANCE_PREFIX, _from);
|
||
|
||
String fromCurrentBalance = stub.getStringState(fromBalanceKey.toString());
|
||
|
||
if (Strings.isNullOrEmpty(fromCurrentBalance)) {
|
||
String errorMessage = String.format("client account %s has no balance", _from);
|
||
throw new ChaincodeException(errorMessage);
|
||
|
||
}
|
||
|
||
long _fromCurrentBalance = Long.parseLong(fromCurrentBalance.toString());
|
||
|
||
// 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(BALANCE_PREFIX, _to);
|
||
String toCurrentBalance = stub.getStringState(toBalanceKey.toString());
|
||
|
||
long _toCurrentBalance = 0;
|
||
// If recipient current balance doesn't yet exist, we'll create it with a
|
||
// current balance of 0
|
||
if (Strings.isNullOrEmpty(toCurrentBalance)) {
|
||
_toCurrentBalance = 0;
|
||
} else {
|
||
_toCurrentBalance = Long.parseLong(toCurrentBalance.trim());
|
||
}
|
||
|
||
// Update the balance
|
||
long fromUpdatedBalance = _fromCurrentBalance - _value;
|
||
long toUpdatedBalance = _toCurrentBalance + _value;
|
||
|
||
stub.putStringState(fromBalanceKey.toString(), String.valueOf(fromUpdatedBalance));
|
||
|
||
stub.putStringState(toBalanceKey.toString(), String.valueOf(toUpdatedBalance));
|
||
|
||
}
|
||
|
||
/**
|
||
* @Desc Allows `spender` to spend `value` amount of tokens from the owner.
|
||
*
|
||
* @param ctx the transaction context
|
||
* @param spender The spender
|
||
* @param value The amount of tokens to be approved for transfer
|
||
* @returns Return whether the approval was successful or not
|
||
*/
|
||
@Transaction()
|
||
public void approve(final Context ctx, final String spender, final String value) {
|
||
|
||
String owner = ctx.getClientIdentity().getId();
|
||
ChaincodeStub stub = ctx.getStub();
|
||
CompositeKey allowanceKey = stub.createCompositeKey(ALLOWANCE_PREFIX, owner, spender);
|
||
long valueInt = Long.parseLong(value);
|
||
stub.putStringState(allowanceKey.toString(), String.valueOf(valueInt));
|
||
stub.setEvent("Approval", new JSONObject().put("owner", owner).put("spender", spender)
|
||
.put(VALUE, valueInt).toString().getBytes(UTF_8));
|
||
|
||
}
|
||
|
||
/**
|
||
* @Desc Returns the amount of tokens which `spender` is allowed to withdraw from `owner`.
|
||
*
|
||
* @param ctx the transaction context
|
||
* @param owner The owner of tokens
|
||
* @param spender The spender who are able to transfer the tokens
|
||
* @returns 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(ALLOWANCE_PREFIX, owner, spender);
|
||
String allowanceBytes = stub.getStringState(allowanceKey.toString());
|
||
|
||
if (Strings.isNullOrEmpty(allowanceBytes)) {
|
||
|
||
String errorMessage = String.format("spender account %s has no allowance from", spender,
|
||
owner);
|
||
throw new ChaincodeException(errorMessage);
|
||
}
|
||
|
||
long allowance = Long.parseLong(allowanceBytes);
|
||
return allowance;
|
||
}
|
||
|
||
/**
|
||
* @Desc Set optional information for a token.
|
||
*
|
||
* @param ctx the transaction context
|
||
* @param name The name of the token
|
||
* @param symbol The symbol of the token
|
||
* @param decimals The decimals of the token
|
||
* @param totalSupply The totalSupply of the token
|
||
*/
|
||
@Transaction()
|
||
public void setOptions(final Context ctx, final String name, final String symbol,
|
||
final String decimals) {
|
||
ChaincodeStub stub = ctx.getStub();
|
||
stub.putStringState(NAME_KEY, name);
|
||
stub.putStringState(SYMBOL_KEY, symbol);
|
||
stub.putStringState(DECIMALS_KEY, decimals);
|
||
|
||
}
|
||
|
||
/**
|
||
* Mint creates new tokens and adds them to minter's account balance
|
||
*
|
||
* @param ctx the transaction context
|
||
* @param amount amount of tokens to be minted
|
||
* @returns The balance
|
||
*/
|
||
@Transaction()
|
||
public void 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(ERC20_OWNER_MSPID)) {
|
||
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(BALANCE_PREFIX, minter);
|
||
|
||
String currentBalanceBytes = stub.getStringState(balanceKey.toString());
|
||
// If minter current balance doesn't yet exist, we'll create it with a current
|
||
// balance of 0
|
||
long currentBalance = 0;
|
||
|
||
if (Strings.isNullOrEmpty(currentBalanceBytes)) {
|
||
|
||
currentBalance = 0;
|
||
|
||
} else {
|
||
|
||
currentBalance = Long.parseLong(currentBalanceBytes);
|
||
|
||
}
|
||
long updatedBalance = currentBalance + amountInt;
|
||
|
||
stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance));
|
||
|
||
// Increase totalSupply
|
||
String totalSupplyBytes = stub.getStringState(TOTAL_SUPPLY_KEY);
|
||
long totalSupply = 0;
|
||
if (Strings.isNullOrEmpty(totalSupplyBytes)) {
|
||
|
||
totalSupply = 0;
|
||
|
||
} else {
|
||
|
||
totalSupply = Long.parseLong(totalSupplyBytes.toString());
|
||
}
|
||
|
||
totalSupply = totalSupply + amountInt;
|
||
stub.putStringState(TOTAL_SUPPLY_KEY, String.valueOf(totalSupply));
|
||
stub.setEvent(TRANSFER_EVENT, new JSONObject().put(FROM, "0x0").put(TO, minter)
|
||
.put(VALUE, amountInt).toString().getBytes(UTF_8));
|
||
|
||
}
|
||
|
||
/**
|
||
* @Desc Burn redeem tokens from minter's account balance.
|
||
* @param ctx the transaction context
|
||
* @param amount amount of tokens to be burned
|
||
* @returns The balance
|
||
*/
|
||
@Transaction()
|
||
public void 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(ERC20_OWNER_MSPID)) {
|
||
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(BALANCE_PREFIX, minter);
|
||
|
||
String currentBalanceBytes = stub.getStringState(balanceKey.toString());
|
||
if (Strings.isNullOrEmpty(currentBalanceBytes)) {
|
||
throw new ChaincodeException("The balance does not exist");
|
||
}
|
||
long currentBalance = Long.valueOf(currentBalanceBytes);
|
||
long updatedBalance = currentBalance - amountInt;
|
||
|
||
stub.putStringState(balanceKey.toString(), String.valueOf(updatedBalance));
|
||
|
||
// Decrease totalSupply
|
||
String totalSupplyBytes = stub.getStringState(TOTAL_SUPPLY_KEY);
|
||
if (Strings.isNullOrEmpty(totalSupplyBytes)) {
|
||
throw new ChaincodeException("totalSupply does not exist.");
|
||
}
|
||
long totalSupply = Long.parseLong(totalSupplyBytes.toString()) - amountInt;
|
||
stub.putStringState(TOTAL_SUPPLY_KEY, String.valueOf(totalSupply));
|
||
|
||
// Emit the Transfer event
|
||
|
||
stub.setEvent(TRANSFER_EVENT, new JSONObject().put(FROM, minter).put(TO, "0x0")
|
||
.put(VALUE, amountInt).toString().getBytes(UTF_8));
|
||
|
||
}
|
||
|
||
/**
|
||
* @Desc: ClientAccountBalance returns the balance of the requesting client's account.
|
||
*
|
||
* @param ctx the transaction context
|
||
* @returns 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(BALANCE_PREFIX, clientAccountID);
|
||
String balanceBytes = stub.getStringState(balanceKey.toString());
|
||
if (Strings.isNullOrEmpty(balanceBytes)) {
|
||
|
||
String errorMessage = String.format("the account %s does not exist", clientAccountID);
|
||
throw new ChaincodeException(errorMessage);
|
||
}
|
||
long balance = Long.parseLong(balanceBytes);
|
||
|
||
return balance;
|
||
}
|
||
|
||
/**
|
||
* @Desc: 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;
|
||
}
|
||
|
||
}
|