mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
JSDoc returns Promises in async functions; Using DRY for ClientAccountID, BalanceOf etc; Using ClientAccountMSPID; Using CheckAuthorization; Using _update for minting, burning and transfering tokens; Additional functions for approving, spending allowance and checking empty values; Updated tests
Signed-off-by: XZSt4nce <xzstnc@mail.ru>
This commit is contained in:
parent
aa0c9d3004
commit
5be225cf5a
2 changed files with 282 additions and 283 deletions
|
|
@ -27,7 +27,7 @@ class TokenERC20Contract extends Contract {
|
|||
* 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
|
||||
* @returns {Promise<String>} Returns the name of the token
|
||||
*/
|
||||
async TokenName(ctx) {
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ class TokenERC20Contract extends Contract {
|
|||
* Return the symbol of the token. E.g. “HIX”.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {String} Returns the symbol of the token
|
||||
* @returns {Promise<String>} Returns the symbol of the token
|
||||
*/
|
||||
async Symbol(ctx) {
|
||||
|
||||
|
|
@ -51,6 +51,7 @@ class TokenERC20Contract extends Contract {
|
|||
await this.CheckInitialized(ctx);
|
||||
|
||||
const symbolBytes = await ctx.stub.getState(symbolKey);
|
||||
|
||||
return symbolBytes.toString();
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ class TokenERC20Contract extends Contract {
|
|||
* 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
|
||||
* @returns {Promise<Number>} Returns the number of decimals
|
||||
*/
|
||||
async Decimals(ctx) {
|
||||
|
||||
|
|
@ -68,6 +69,7 @@ class TokenERC20Contract extends Contract {
|
|||
|
||||
const decimalsBytes = await ctx.stub.getState(decimalsKey);
|
||||
const decimals = parseInt(decimalsBytes.toString());
|
||||
|
||||
return decimals;
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +77,7 @@ class TokenERC20Contract extends Contract {
|
|||
* Return the total token supply.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {Number} Returns the total token supply
|
||||
* @returns {Promise<Number>} Returns the total token supply
|
||||
*/
|
||||
async TotalSupply(ctx) {
|
||||
|
||||
|
|
@ -84,6 +86,7 @@ class TokenERC20Contract extends Contract {
|
|||
|
||||
const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey);
|
||||
const totalSupply = parseInt(totalSupplyBytes.toString());
|
||||
|
||||
return totalSupply;
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +95,7 @@ class TokenERC20Contract extends Contract {
|
|||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} owner The owner from which the balance will be retrieved
|
||||
* @returns {Number} Returns the account balance
|
||||
* @returns {Promise<Number>} Returns the account balance
|
||||
*/
|
||||
async BalanceOf(ctx, owner) {
|
||||
|
||||
|
|
@ -100,12 +103,8 @@ class TokenERC20Contract extends Contract {
|
|||
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());
|
||||
const balance = this.isEmpty(balanceBytes) ? 0 : parseInt(balanceBytes.toString());
|
||||
|
||||
return balance;
|
||||
}
|
||||
|
|
@ -116,25 +115,18 @@ class TokenERC20Contract extends Contract {
|
|||
*
|
||||
* @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
|
||||
* @param {Number} value The amount of token to be transferred
|
||||
* @returns {Promise<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)));
|
||||
const from = this.ClientAccountID(ctx);
|
||||
await this._transfer(ctx, from, to, value);
|
||||
|
||||
console.log('transfer ended successfully');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -144,103 +136,42 @@ class TokenERC20Contract extends Contract {
|
|||
* @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
|
||||
* @param {Number} value The amount of token to be transferred
|
||||
* @returns {Promise<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)));
|
||||
const spender = this.ClientAccountID(ctx);
|
||||
await this._spendAllowance(ctx, from, spender, value);
|
||||
await this._transfer(ctx, from, to, value);
|
||||
|
||||
console.log('transferFrom ended successfully');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a `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 {Promise<Number>} value The amount of token to be transferred
|
||||
*/
|
||||
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');
|
||||
if (this.isEmpty(from)) {
|
||||
throw new Error('invalid sender');
|
||||
}
|
||||
if (this.isEmpty(to)) {
|
||||
throw new Error('invalid receiver');
|
||||
}
|
||||
|
||||
// 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;
|
||||
await this._update(ctx, from, to, value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -248,26 +179,18 @@ class TokenERC20Contract extends Contract {
|
|||
*
|
||||
* @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
|
||||
* @param {Number} value The amount of tokens to be approved for transfer
|
||||
* @returns {Promise<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)));
|
||||
|
||||
const owner = this.ClientAccountID(ctx);
|
||||
await this._approve(ctx, owner, spender, value);
|
||||
console.log('approve ended successfully');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +200,7 @@ class TokenERC20Contract extends Contract {
|
|||
* @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
|
||||
* @returns {Promise<Number>} Return the amount of remaining tokens allowed to spent
|
||||
*/
|
||||
async Allowance(ctx, owner, spender) {
|
||||
|
||||
|
|
@ -287,11 +210,8 @@ class TokenERC20Contract extends Contract {
|
|||
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 = this.isEmpty(allowanceBytes) ? 0 : parseInt(allowanceBytes.toString());
|
||||
|
||||
const allowance = parseInt(allowanceBytes.toString());
|
||||
return allowance;
|
||||
}
|
||||
|
||||
|
|
@ -307,21 +227,19 @@ class TokenERC20Contract extends Contract {
|
|||
* @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 client authorization
|
||||
this.CheckAuthorization(ctx);
|
||||
|
||||
// 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 && nameBytes.length > 0) {
|
||||
if (!this.isEmpty(nameBytes)) {
|
||||
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));
|
||||
await ctx.stub.putState(totalSupplyKey, Buffer.from('0'));
|
||||
|
||||
console.log(`name: ${name}, symbol: ${symbol}, decimals: ${decimals}`);
|
||||
return true;
|
||||
|
|
@ -331,108 +249,41 @@ class TokenERC20Contract extends Contract {
|
|||
* 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
|
||||
* @param {Number} amount amount of tokens to be minted
|
||||
* @returns {Promise<Boolean>} Return whether the mint was successful or not
|
||||
*/
|
||||
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');
|
||||
}
|
||||
// Check minter authorization
|
||||
this.CheckAuthorization(ctx);
|
||||
|
||||
// Get ID of submitting client identity
|
||||
const minter = ctx.clientIdentity.getID();
|
||||
const minter = this.ClientAccountID(ctx);
|
||||
await this._update(ctx, '', minter, amount);
|
||||
|
||||
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
|
||||
* Burn redeem tokens from burner's account balance
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {Integer} amount amount of tokens to be burned
|
||||
* @returns {Object} The balance
|
||||
* @param {Number} amount amount of tokens to be burned
|
||||
* @returns {Promise<Boolean>} Return whether the burn was successful or not
|
||||
*/
|
||||
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');
|
||||
}
|
||||
// Check burner authorization
|
||||
this.CheckAuthorization(ctx);
|
||||
|
||||
const minter = ctx.clientIdentity.getID();
|
||||
const burner = this.ClientAccountID(ctx);
|
||||
await this._update(ctx, burner, '', amount);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -440,7 +291,7 @@ class TokenERC20Contract extends Contract {
|
|||
* ClientAccountBalance returns the balance of the requesting client's account.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {Number} Returns the account balance
|
||||
* @returns {Promise<Number>} Returns the account balance
|
||||
*/
|
||||
async ClientAccountBalance(ctx) {
|
||||
|
||||
|
|
@ -448,39 +299,184 @@ class TokenERC20Contract extends Contract {
|
|||
await this.CheckInitialized(ctx);
|
||||
|
||||
// Get ID of submitting client identity
|
||||
const clientAccountID = ctx.clientIdentity.getID();
|
||||
const clientAccountID = this.ClientAccountID(ctx);
|
||||
|
||||
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;
|
||||
return await this.BalanceOf(ctx, clientAccountID);
|
||||
}
|
||||
|
||||
// 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);
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {String} Returns the account id
|
||||
*/
|
||||
ClientAccountID(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){
|
||||
/**
|
||||
* ClientAccountMSPID returns the MSP id of the requesting client's account.
|
||||
* In this implementation, the client account MSP ID is the clientMspId itself.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @returns {String} Returns the account MSP id
|
||||
*/
|
||||
ClientAccountMSPID(ctx) {
|
||||
|
||||
// Get ID of submitting client identity
|
||||
const clientAccountMSPID = ctx.clientIdentity.getMSPID();
|
||||
return clientAccountMSPID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that contract options have been already initialized
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
*/
|
||||
async CheckInitialized(ctx) {
|
||||
const nameBytes = await ctx.stub.getState(nameKey);
|
||||
if (!nameBytes || nameBytes.length === 0) {
|
||||
if (this.isEmpty(nameBytes)) {
|
||||
throw new Error('contract options need to be set before calling any function, call Initialize() to initialize contract');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check client authorization - this sample assumes Org1 is the central banker with privilege to burn tokens
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
*/
|
||||
CheckAuthorization(ctx) {
|
||||
const clientMSPID = this.ClientAccountMSPID(ctx);
|
||||
if (clientMSPID !== 'Org1MSP') {
|
||||
throw new Error('client is not authorized');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
|
||||
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
|
||||
* this function.
|
||||
*
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} from The sender
|
||||
* @param {String} to The recipient
|
||||
* @param {Number} value The amount of token to be transferred
|
||||
*/
|
||||
async _update(ctx, from, to, value) {
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
let totalSupply = await this.TotalSupply(ctx);
|
||||
if (this.isEmpty(from)) {
|
||||
// Overflow check required: The rest of the code assumes that totalSupply never overflows
|
||||
totalSupply = this.add(totalSupply, valueInt);
|
||||
} else {
|
||||
// Retrieve the current balance of the sender
|
||||
const fromCurrentBalance = await this.BalanceOf(ctx, from);
|
||||
// Check if the sender has enough tokens to spend.
|
||||
if (fromCurrentBalance < valueInt) {
|
||||
throw new Error(`client account ${from} has insufficient funds.`);
|
||||
}
|
||||
// Overflow not possible: valueInt <= fromCurrentBalance <= totalSupply.
|
||||
const fromBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [from]);
|
||||
const fromUpdatedBalance = fromCurrentBalance - valueInt;
|
||||
await ctx.stub.putState(fromBalanceKey, Buffer.from(fromUpdatedBalance.toString()));
|
||||
console.log(`client ${from} balance updated from ${fromCurrentBalance} to ${fromUpdatedBalance}`);
|
||||
}
|
||||
if (this.isEmpty(to)) {
|
||||
// Overflow not possible: valueInt <= totalSupply.
|
||||
totalSupply -= valueInt;
|
||||
} else {
|
||||
// Overflow not possible: toCurrentBalance + valueInt is at most totalSupply
|
||||
const toCurrentBalance = await this.BalanceOf(ctx, to);
|
||||
const toBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [to]);
|
||||
const toUpdatedBalance = toCurrentBalance + valueInt;
|
||||
await ctx.stub.putState(toBalanceKey, Buffer.from(toUpdatedBalance.toString()));
|
||||
console.log(`recipient ${to} balance updated from ${toCurrentBalance} to ${toUpdatedBalance}`);
|
||||
}
|
||||
|
||||
await ctx.stub.putState(totalSupplyKey, Buffer.from(totalSupply.toString()));
|
||||
|
||||
// Emit the Transfer event
|
||||
const transferEvent = { from, to, value: valueInt };
|
||||
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} owner The owner of tokens
|
||||
* @param {String} spender The spender
|
||||
* @param {Number} value The amount of token to be transferred
|
||||
*/
|
||||
async _approve(ctx, owner, spender, value) {
|
||||
await this._approveEvent(ctx, owner, spender, value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} owner The owner of tokens
|
||||
* @param {String} spender The spender
|
||||
* @param {Number} value The amount of token to be transferred
|
||||
* @param {Boolean} emitEvent Whether to emit event
|
||||
*/
|
||||
async _approveEvent(ctx, owner, spender, value, emitEvent) {
|
||||
if (this.isEmpty(owner)) {
|
||||
throw new Error('invalid approver');
|
||||
}
|
||||
if (this.isEmpty(spender)) {
|
||||
throw new Error('invalid spender');
|
||||
}
|
||||
|
||||
const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]);
|
||||
await ctx.stub.putState(allowanceKey, Buffer.from(value.toString()));
|
||||
|
||||
// Emit the Approval event
|
||||
if (emitEvent) {
|
||||
const valueInt = parseInt(value);
|
||||
const approvalEvent = { owner, spender, value: valueInt };
|
||||
ctx.stub.setEvent('Approval', Buffer.from(JSON.stringify(approvalEvent)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Context} ctx the transaction context
|
||||
* @param {String} owner The owner of tokens
|
||||
* @param {String} spender The spender
|
||||
* @param {Number} value The amount of token to be transferred
|
||||
*/
|
||||
async _spendAllowance(ctx, owner, spender, value) {
|
||||
|
||||
// Retrieve the allowance of the spender
|
||||
const currentAllowance = await this.Allowance(ctx, owner, spender);
|
||||
|
||||
// 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.');
|
||||
}
|
||||
// Decrease the allowance
|
||||
const updatedAllowance = currentAllowance - valueInt;
|
||||
await this._approveEvent(ctx, owner, spender, updatedAllowance, false);
|
||||
console.log(`spender ${spender} allowance updated from ${currentAllowance} to ${updatedAllowance}`);
|
||||
}
|
||||
|
||||
// Return whether the value is empty or not
|
||||
isEmpty(value) {
|
||||
return (!value || value.length === 0);
|
||||
}
|
||||
|
||||
// add two number checking for overflow
|
||||
add(a, b) {
|
||||
let c = a + b;
|
||||
|
|
|
|||
|
|
@ -20,15 +20,13 @@ chai.should();
|
|||
chai.use(chaiAsPromised);
|
||||
|
||||
describe('Chaincode', () => {
|
||||
let sandbox;
|
||||
let token;
|
||||
let ctx;
|
||||
let mockStub;
|
||||
let mockClientIdentity;
|
||||
|
||||
beforeEach('Sandbox creation', async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
token = new TokenERC20Contract('token-erc20');
|
||||
beforeEach(async () => {
|
||||
token = new TokenERC20Contract();
|
||||
|
||||
ctx = sinon.createStubInstance(Context);
|
||||
mockStub = sinon.createStubInstance(ChaincodeStub);
|
||||
|
|
@ -36,41 +34,34 @@ describe('Chaincode', () => {
|
|||
mockClientIdentity = sinon.createStubInstance(ClientIdentity);
|
||||
ctx.clientIdentity = mockClientIdentity;
|
||||
|
||||
mockClientIdentity.getMSPID.returns('Org1MSP');
|
||||
|
||||
await token.Initialize(ctx, 'some name', 'some symbol', '2');
|
||||
|
||||
mockStub.putState.resolves('some state');
|
||||
mockStub.setEvent.returns('set event');
|
||||
|
||||
});
|
||||
|
||||
afterEach('Sandbox restoration', () => {
|
||||
sandbox.restore();
|
||||
mockStub.getState.withArgs('name').resolves(Buffer.from('some name'));
|
||||
mockStub.getState.withArgs('symbol').resolves(Buffer.from('some symbol'));
|
||||
mockStub.getState.withArgs('decimals').resolves(Buffer.from('2'));
|
||||
mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('0'));
|
||||
console.log('Initialized');
|
||||
});
|
||||
|
||||
describe('#TokenName', () => {
|
||||
it('should work', async () => {
|
||||
mockStub.getState.resolves('some state');
|
||||
|
||||
const response = await token.TokenName(ctx);
|
||||
sinon.assert.calledWith(mockStub.getState, 'name');
|
||||
expect(response).to.equals('some state');
|
||||
expect(response).to.equals('some name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#Symbol', () => {
|
||||
it('should work', async () => {
|
||||
mockStub.getState.resolves('some state');
|
||||
|
||||
const response = await token.Symbol(ctx);
|
||||
sinon.assert.calledWith(mockStub.getState, 'symbol');
|
||||
expect(response).to.equals('some state');
|
||||
expect(response).to.equals('some symbol');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#Decimals', () => {
|
||||
it('should work', async () => {
|
||||
mockStub.getState.resolves(Buffer.from('2'));
|
||||
|
||||
const response = await token.Decimals(ctx);
|
||||
sinon.assert.calledWith(mockStub.getState, 'decimals');
|
||||
expect(response).to.equals(2);
|
||||
|
|
@ -79,11 +70,9 @@ describe('Chaincode', () => {
|
|||
|
||||
describe('#TotalSupply', () => {
|
||||
it('should work', async () => {
|
||||
mockStub.getState.resolves(Buffer.from('10000'));
|
||||
|
||||
const response = await token.TotalSupply(ctx);
|
||||
sinon.assert.calledWith(mockStub.getState, 'totalSupply');
|
||||
expect(response).to.equals(10000);
|
||||
expect(response).to.equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -100,16 +89,16 @@ describe('Chaincode', () => {
|
|||
describe('#_transfer', () => {
|
||||
|
||||
it('should fail when the sender and the receipient are the same', async () => {
|
||||
await expect(token._transfer(ctx, 'Alice', 'Alice', '1000'))
|
||||
.to.be.rejectedWith(Error, 'cannot transfer to and from same client account');
|
||||
const response = token._transfer(ctx, 'Alice', 'Alice', '1000');
|
||||
await expect(response).to.be.rejectedWith(Error, 'cannot transfer to and from same client account');
|
||||
});
|
||||
|
||||
it('should fail when the sender does not have enough token', async () => {
|
||||
mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice');
|
||||
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('500'));
|
||||
|
||||
await expect(token._transfer(ctx, 'Alice', 'Bob', '1000'))
|
||||
.to.be.rejectedWith(Error, 'client account Alice has insufficient funds.');
|
||||
const response = token._transfer(ctx, 'Alice', 'Bob', '1000');
|
||||
await expect(response).to.be.rejectedWith(Error, 'client account Alice has insufficient funds.');
|
||||
});
|
||||
|
||||
it('should transfer to a new account when the sender has enough token', async () => {
|
||||
|
|
@ -119,10 +108,9 @@ describe('Chaincode', () => {
|
|||
mockStub.createCompositeKey.withArgs('balance', ['Bob']).returns('balance_Bob');
|
||||
mockStub.getState.withArgs('balance_Bob').resolves(null);
|
||||
|
||||
const response = await token._transfer(ctx, 'Alice', 'Bob', '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(1), 'balance_Bob', Buffer.from('1000'));
|
||||
expect(response).to.equals(true);
|
||||
await token._transfer(ctx, 'Alice', 'Bob', '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(4), 'balance_Alice', Buffer.from('0'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(5), 'balance_Bob', Buffer.from('1000'));
|
||||
});
|
||||
|
||||
it('should transfer to the existing account when the sender has enough token', async () => {
|
||||
|
|
@ -132,10 +120,9 @@ describe('Chaincode', () => {
|
|||
mockStub.createCompositeKey.withArgs('balance', ['Bob']).returns('balance_Bob');
|
||||
mockStub.getState.withArgs('balance_Bob').resolves(Buffer.from('2000'));
|
||||
|
||||
const response = await token._transfer(ctx, 'Alice', 'Bob', '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(1), 'balance_Bob', Buffer.from('3000'));
|
||||
expect(response).to.equals(true);
|
||||
await token._transfer(ctx, 'Alice', 'Bob', '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(4), 'balance_Alice', Buffer.from('0'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(5), 'balance_Bob', Buffer.from('3000'));
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -143,7 +130,9 @@ describe('Chaincode', () => {
|
|||
describe('#Transfer', () => {
|
||||
it('should work', async () => {
|
||||
mockClientIdentity.getID.returns('Alice');
|
||||
sinon.stub(token, '_transfer').returns(true);
|
||||
|
||||
mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice');
|
||||
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
|
||||
|
||||
const response = await token.Transfer(ctx, 'Bob', '1000');
|
||||
const event = { from: 'Alice', to: 'Bob', value: 1000 };
|
||||
|
|
@ -159,18 +148,19 @@ describe('Chaincode', () => {
|
|||
mockStub.createCompositeKey.withArgs('allowance', ['Alice', 'Charlie']).returns('allowance_Alice_Charlie');
|
||||
mockStub.getState.withArgs('allowance_Alice_Charlie').resolves(Buffer.from('0'));
|
||||
|
||||
await expect(token.TransferFrom(ctx, 'Alice', 'Bob', '1000'))
|
||||
.to.be.rejectedWith(Error, 'The spender does not have enough allowance to spend.');
|
||||
const response = token.TransferFrom(ctx, 'Alice', 'Bob', '1000');
|
||||
await expect(response).to.be.rejectedWith(Error, 'The spender does not have enough allowance to spend.');
|
||||
});
|
||||
|
||||
it('should transfer when the spender is allowed to spend the token', async () => {
|
||||
mockClientIdentity.getID.returns('Charlie');
|
||||
|
||||
mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice');
|
||||
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
|
||||
|
||||
mockStub.createCompositeKey.withArgs('allowance', ['Alice', 'Charlie']).returns('allowance_Alice_Charlie');
|
||||
mockStub.getState.withArgs('allowance_Alice_Charlie').resolves(Buffer.from('3000'));
|
||||
|
||||
sinon.stub(token, '_transfer').returns(true);
|
||||
|
||||
const response = await token.TransferFrom(ctx, 'Alice', 'Bob', '1000');
|
||||
sinon.assert.calledWith(mockStub.putState, 'allowance_Alice_Charlie', Buffer.from('2000'));
|
||||
const event = { from: 'Alice', to: 'Bob', value: 1000 };
|
||||
|
|
@ -210,48 +200,43 @@ describe('Chaincode', () => {
|
|||
|
||||
it('should failed if called a second time', async () => {
|
||||
// We consider it has already been initialized in the before-each statement
|
||||
await expect(await token.Initialize(ctx, 'some name', 'some symbol', '2'))
|
||||
.to.be.rejectedWith(Error, 'contract options are already set, client is not authorized to change them');
|
||||
const response = token.Initialize(ctx, 'some name', 'some symbol', '2');
|
||||
await expect(response).to.be.rejectedWith(Error, 'contract options are already set, client is not authorized to change them');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#Mint', () => {
|
||||
it('should add token to a new account and a new total supply', async () => {
|
||||
mockClientIdentity.getMSPID.returns('Org1MSP');
|
||||
mockClientIdentity.getID.returns('Alice');
|
||||
mockStub.createCompositeKey.returns('balance_Alice');
|
||||
mockStub.getState.withArgs('balance_Alice').resolves(null);
|
||||
mockStub.getState.withArgs('totalSupply').resolves(null);
|
||||
|
||||
const response = await token.Mint(ctx, '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('1000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('1000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(4), 'balance_Alice', Buffer.from('1000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(5), 'totalSupply', Buffer.from('1000'));
|
||||
expect(response).to.equals(true);
|
||||
});
|
||||
|
||||
it('should add token to the existing account and the existing total supply', async () => {
|
||||
mockClientIdentity.getMSPID.returns('Org1MSP');
|
||||
mockClientIdentity.getID.returns('Alice');
|
||||
mockStub.createCompositeKey.returns('balance_Alice');
|
||||
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
|
||||
mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000'));
|
||||
|
||||
const response = await token.Mint(ctx, '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('2000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('3000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(4), 'balance_Alice', Buffer.from('2000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(5), 'totalSupply', Buffer.from('3000'));
|
||||
expect(response).to.equals(true);
|
||||
});
|
||||
|
||||
it('should add token to a new account and the existing total supply', async () => {
|
||||
mockClientIdentity.getMSPID.returns('Org1MSP');
|
||||
mockClientIdentity.getID.returns('Alice');
|
||||
mockStub.createCompositeKey.returns('balance_Alice');
|
||||
mockStub.getState.withArgs('balance_Alice').resolves(null);
|
||||
mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000'));
|
||||
|
||||
const response = await token.Mint(ctx, '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('1000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('3000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(4), 'balance_Alice', Buffer.from('1000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(5), 'totalSupply', Buffer.from('3000'));
|
||||
expect(response).to.equals(true);
|
||||
});
|
||||
|
||||
|
|
@ -259,15 +244,14 @@ describe('Chaincode', () => {
|
|||
|
||||
describe('#Burn', () => {
|
||||
it('should work', async () => {
|
||||
mockClientIdentity.getMSPID.returns('Org1MSP');
|
||||
mockClientIdentity.getID.returns('Alice');
|
||||
mockStub.createCompositeKey.returns('balance_Alice');
|
||||
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
|
||||
mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000'));
|
||||
|
||||
const response = await token.Burn(ctx, '1000');
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('1000'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(4), 'balance_Alice', Buffer.from('0'));
|
||||
sinon.assert.calledWith(mockStub.putState.getCall(5), 'totalSupply', Buffer.from('1000'));
|
||||
expect(response).to.equals(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -278,7 +262,7 @@ describe('Chaincode', () => {
|
|||
mockStub.createCompositeKey.returns('balance_Alice');
|
||||
mockStub.getState.resolves(Buffer.from('1000'));
|
||||
|
||||
const response = await token.ClientAccountBalance(ctx,);
|
||||
const response = await token.ClientAccountBalance(ctx);
|
||||
expect(response).to.equals(1000);
|
||||
});
|
||||
});
|
||||
|
|
@ -293,4 +277,23 @@ describe('Chaincode', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#ClientAccountMSPID', () => {
|
||||
it('should work', async () => {
|
||||
const response = await token.ClientAccountMSPID(ctx);
|
||||
sinon.assert.calledTwice(mockClientIdentity.getMSPID);
|
||||
expect(response).to.equals('Org1MSP');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CheckAuthorization', () => {
|
||||
it('should work', async () => {
|
||||
await token.CheckAuthorization(ctx);
|
||||
});
|
||||
|
||||
it('should failed if called by not Org1MSP', () => {
|
||||
mockClientIdentity.getMSPID.returns('Org2MSP');
|
||||
expect(() => token.CheckAuthorization(ctx)).to.throw(Error, 'client is not authorized');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue