fabric-samples/token-erc-20/chaincode-javascript/lib/tokenERC20.js
fraVlaca 5f71466295
updated erc tokens samples (#731)
Signed-off-by: fraVlaca <ocsenarf@outlook.com>
2022-05-11 10:35:10 -04:00

503 lines
19 KiB
JavaScript

/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
'use strict';
const { Contract } = require('fabric-contract-api');
// Define objectType names for prefix
const balancePrefix = 'balance';
const allowancePrefix = 'allowance';
// Define key names for options
const nameKey = 'name';
const symbolKey = 'symbol';
const decimalsKey = 'decimals';
const totalSupplyKey = 'totalSupply';
class TokenERC20Contract extends Contract {
/**
* 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
*/
async TokenName(ctx) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const nameBytes = await ctx.stub.getState(nameKey);
return nameBytes.toString();
}
/**
* Return the symbol of the token. E.g. “HIX”.
*
* @param {Context} ctx the transaction context
* @returns {String} Returns the symbol of the token
*/
async Symbol(ctx) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const symbolBytes = await ctx.stub.getState(symbolKey);
return symbolBytes.toString();
}
/**
* 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
*/
async Decimals(ctx) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const decimalsBytes = await ctx.stub.getState(decimalsKey);
const decimals = parseInt(decimalsBytes.toString());
return decimals;
}
/**
* Return the total token supply.
*
* @param {Context} ctx the transaction context
* @returns {Number} Returns the total token supply
*/
async TotalSupply(ctx) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey);
const totalSupply = parseInt(totalSupplyBytes.toString());
return 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
*/
async BalanceOf(ctx, owner) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [owner]);
const balanceBytes = await ctx.stub.getState(balanceKey);
if (!balanceBytes || balanceBytes.length === 0) {
throw new Error(`the account ${owner} does not exist`);
}
const balance = parseInt(balanceBytes.toString());
return balance;
}
/**
* 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
*/
async Transfer(ctx, to, value) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const from = ctx.clientIdentity.getID();
const transferResp = await this._transfer(ctx, from, to, value);
if (!transferResp) {
throw new Error('Failed to transfer');
}
// Emit the Transfer event
const transferEvent = { from, to, value: parseInt(value) };
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
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
*/
async TransferFrom(ctx, from, to, value) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const spender = ctx.clientIdentity.getID();
// Retrieve the allowance of the spender
const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [from, spender]);
const currentAllowanceBytes = await ctx.stub.getState(allowanceKey);
if (!currentAllowanceBytes || currentAllowanceBytes.length === 0) {
throw new Error(`spender ${spender} has no allowance from ${from}`);
}
const currentAllowance = parseInt(currentAllowanceBytes.toString());
// Convert value from string to int
const valueInt = parseInt(value);
// Check if the transferred value is less than the allowance
if (currentAllowance < valueInt) {
throw new Error('The spender does not have enough allowance to spend.');
}
const transferResp = await this._transfer(ctx, from, to, value);
if (!transferResp) {
throw new Error('Failed to transfer');
}
// Decrease the allowance
const updatedAllowance = this.sub(currentAllowance, valueInt);
await ctx.stub.putState(allowanceKey, Buffer.from(updatedAllowance.toString()));
console.log(`spender ${spender} allowance updated from ${currentAllowance} to ${updatedAllowance}`);
// Emit the Transfer event
const transferEvent = { from, to, value: valueInt };
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
console.log('transferFrom ended successfully');
return true;
}
async _transfer(ctx, from, to, value) {
if (from === to) {
throw new Error('cannot transfer to and from same client account');
}
// Convert value from string to int
const valueInt = parseInt(value);
if (valueInt < 0) { // transfer of 0 is allowed in ERC20, so just validate against negative amounts
throw new Error('transfer amount cannot be negative');
}
// Retrieve the current balance of the sender
const fromBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [from]);
const fromCurrentBalanceBytes = await ctx.stub.getState(fromBalanceKey);
if (!fromCurrentBalanceBytes || fromCurrentBalanceBytes.length === 0) {
throw new Error(`client account ${from} has no balance`);
}
const fromCurrentBalance = parseInt(fromCurrentBalanceBytes.toString());
// Check if the sender has enough tokens to spend.
if (fromCurrentBalance < valueInt) {
throw new Error(`client account ${from} has insufficient funds.`);
}
// Retrieve the current balance of the recepient
const toBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [to]);
const toCurrentBalanceBytes = await ctx.stub.getState(toBalanceKey);
let toCurrentBalance;
// If recipient current balance doesn't yet exist, we'll create it with a current balance of 0
if (!toCurrentBalanceBytes || toCurrentBalanceBytes.length === 0) {
toCurrentBalance = 0;
} else {
toCurrentBalance = parseInt(toCurrentBalanceBytes.toString());
}
// Update the balance
const fromUpdatedBalance = this.sub(fromCurrentBalance, valueInt);
const toUpdatedBalance = this.add(toCurrentBalance, valueInt);
await ctx.stub.putState(fromBalanceKey, Buffer.from(fromUpdatedBalance.toString()));
await ctx.stub.putState(toBalanceKey, Buffer.from(toUpdatedBalance.toString()));
console.log(`client ${from} balance updated from ${fromCurrentBalance} to ${fromUpdatedBalance}`);
console.log(`recipient ${to} balance updated from ${toCurrentBalance} to ${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
*/
async Approve(ctx, spender, value) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const owner = ctx.clientIdentity.getID();
const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]);
let valueInt = parseInt(value);
await ctx.stub.putState(allowanceKey, Buffer.from(valueInt.toString()));
// Emit the Approval event
const approvalEvent = { owner, spender, value: valueInt };
ctx.stub.setEvent('Approval', Buffer.from(JSON.stringify(approvalEvent)));
console.log('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
*/
async Allowance(ctx, owner, spender) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]);
const allowanceBytes = await ctx.stub.getState(allowanceKey);
if (!allowanceBytes || allowanceBytes.length === 0) {
throw new Error(`spender ${spender} has no allowance from ${owner}`);
}
const allowance = parseInt(allowanceBytes.toString());
return allowance;
}
// ================== Extended Functions ==========================
/**
* 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
*/
async Initialize(ctx, name, symbol, decimals) {
// Check minter authorization - this sample assumes Org1 is the central banker with privilege to set Options for these tokens
const clientMSPID = ctx.clientIdentity.getMSPID();
if (clientMSPID !== 'Org1MSP') {
throw new Error('client is not authorized to initialize contract');
}
//check contract options are not already set, client is not authorized to change them once intitialized
const nameBytes = await ctx.stub.getState(nameKey);
if (nameBytes !== undefined) {
throw new Error('contract options are already set, client is not authorized to change them');
}
await ctx.stub.putState(nameKey, Buffer.from(name));
await ctx.stub.putState(symbolKey, Buffer.from(symbol));
await ctx.stub.putState(decimalsKey, Buffer.from(decimals));
console.log(`name: ${name}, symbol: ${symbol}, decimals: ${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
*/
async Mint(ctx, amount) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
// Check minter authorization - this sample assumes Org1 is the central banker with privilege to mint new tokens
const clientMSPID = ctx.clientIdentity.getMSPID();
if (clientMSPID !== 'Org1MSP') {
throw new Error('client is not authorized to mint new tokens');
}
// Get ID of submitting client identity
const minter = ctx.clientIdentity.getID();
const amountInt = parseInt(amount);
if (amountInt <= 0) {
throw new Error('mint amount must be a positive integer');
}
const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]);
const currentBalanceBytes = await ctx.stub.getState(balanceKey);
// If minter current balance doesn't yet exist, we'll create it with a current balance of 0
let currentBalance;
if (!currentBalanceBytes || currentBalanceBytes.length === 0) {
currentBalance = 0;
} else {
currentBalance = parseInt(currentBalanceBytes.toString());
}
const updatedBalance = this.add(currentBalance, amountInt);
await ctx.stub.putState(balanceKey, Buffer.from(updatedBalance.toString()));
// Increase totalSupply
const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey);
let totalSupply;
if (!totalSupplyBytes || totalSupplyBytes.length === 0) {
console.log('Initialize the tokenSupply');
totalSupply = 0;
} else {
totalSupply = parseInt(totalSupplyBytes.toString());
}
totalSupply = this.add(totalSupply, amountInt);
await ctx.stub.putState(totalSupplyKey, Buffer.from(totalSupply.toString()));
// Emit the Transfer event
const transferEvent = { from: '0x0', to: minter, value: amountInt };
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
console.log(`minter account ${minter} balance updated from ${currentBalance} to ${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
*/
async Burn(ctx, amount) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
// Check minter authorization - this sample assumes Org1 is the central banker with privilege to burn tokens
const clientMSPID = ctx.clientIdentity.getMSPID();
if (clientMSPID !== 'Org1MSP') {
throw new Error('client is not authorized to mint new tokens');
}
const minter = ctx.clientIdentity.getID();
const amountInt = parseInt(amount);
const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]);
const currentBalanceBytes = await ctx.stub.getState(balanceKey);
if (!currentBalanceBytes || currentBalanceBytes.length === 0) {
throw new Error('The balance does not exist');
}
const currentBalance = parseInt(currentBalanceBytes.toString());
const updatedBalance = this.sub(currentBalance, amountInt);
await ctx.stub.putState(balanceKey, Buffer.from(updatedBalance.toString()));
// Decrease totalSupply
const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey);
if (!totalSupplyBytes || totalSupplyBytes.length === 0) {
throw new Error('totalSupply does not exist.');
}
const totalSupply = this.sub(parseInt(totalSupplyBytes.toString()), amountInt);
await ctx.stub.putState(totalSupplyKey, Buffer.from(totalSupply.toString()));
// Emit the Transfer event
const transferEvent = { from: minter, to: '0x0', value: amountInt };
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
console.log(`minter account ${minter} balance updated from ${currentBalance} to ${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
*/
async ClientAccountBalance(ctx) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
// Get ID of submitting client identity
const clientAccountID = ctx.clientIdentity.getID();
const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [clientAccountID]);
const balanceBytes = await ctx.stub.getState(balanceKey);
if (!balanceBytes || balanceBytes.length === 0) {
throw new Error(`the account ${clientAccountID} does not exist`);
}
const balance = parseInt(balanceBytes.toString());
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
async ClientAccountID(ctx) {
//check contract options are already set first to execute the function
await this.CheckInitialized(ctx);
// Get ID of submitting client identity
const clientAccountID = ctx.clientIdentity.getID();
return clientAccountID;
}
//Checks that contract options have been already initialized
async CheckInitialized(ctx){
const nameBytes = await ctx.stub.getState(nameKey);
if (nameBytes === undefined) {
throw new Error('contract options need to be set before calling any function, call Initialize() to initialize contract');
}
}
// add two number checking for overflow
add(a, b) {
let c = a + b;
if (a !== c - b || b !== c - a){
throw new Error(`Math: addition overflow occurred ${a} + ${b}`);
}
return c;
}
// add two number checking for overflow
sub(a, b) {
let c = a - b;
if (a !== c + b || b !== a - c){
throw new Error(`Math: subtraction overflow occurred ${a} - ${b}`);
}
return c;
}
}
module.exports = TokenERC20Contract;