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

This commit is contained in:
XZSt4nce 2025-03-08 02:26:02 +03:00
parent aa0c9d3004
commit f4b092bed9
2 changed files with 282 additions and 283 deletions

View file

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

View file

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