diff --git a/token-erc-20/chaincode-javascript/lib/tokenERC20.js b/token-erc-20/chaincode-javascript/lib/tokenERC20.js index cb4ef24411..4373ea1c37 100644 --- a/token-erc-20/chaincode-javascript/lib/tokenERC20.js +++ b/token-erc-20/chaincode-javascript/lib/tokenERC20.js @@ -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} 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} 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} 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} 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} 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} 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} 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} 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'); - } - - // 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.`); + if (this.isEmpty(from)) { + throw new Error('invalid sender'); } - - // 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()); + if (this.isEmpty(to)) { + throw new Error('invalid receiver'); } - // 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} 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} 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; } @@ -304,24 +224,21 @@ class TokenERC20Contract extends Contract { * @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 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,154 +248,232 @@ 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} 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'); - } - - // Get ID of submitting client identity - const minter = ctx.clientIdentity.getID(); + // Check minter authorization + this.CheckAuthorization(ctx); - const amountInt = parseInt(amount); - if (amountInt <= 0) { - throw new Error('mint amount must be a positive integer'); - } + const minter = this.ClientAccountID(ctx); + await this._update(ctx, '', minter, amount); - const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]); + return true; + } - 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); + /** + * Burn redeem tokens from burner's account balance + * + * @param {Context} ctx the transaction context + * @param {Number} amount amount of tokens to be burned + * @returns {Promise} Return whether the burn was successful or not + */ + async Burn(ctx, amount) { - await ctx.stub.putState(balanceKey, Buffer.from(updatedBalance.toString())); + // Check contract options are already set first to execute the function + await this.CheckInitialized(ctx); - // 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())); + // Check burner authorization + this.CheckAuthorization(ctx); - // Emit the Transfer event - const transferEvent = { from: '0x0', to: minter, value: amountInt }; - ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + const burner = this.ClientAccountID(ctx); + await this._update(ctx, burner, '', amount); - console.log(`minter account ${minter} balance updated from ${currentBalance} to ${updatedBalance}`); return true; } /** - * Burn redeem tokens from minter's account balance + * ClientAccountBalance returns the balance of the requesting client's account. * * @param {Context} ctx the transaction context - * @param {Integer} amount amount of tokens to be burned - * @returns {Object} The balance + * @returns {Promise} Returns the account balance */ - async Burn(ctx, amount) { + async ClientAccountBalance(ctx) { // 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'); - } + // Get ID of submitting client identity + const clientAccountID = this.ClientAccountID(ctx); + + 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 + * + * @param {Context} ctx the transaction context + * @returns {String} Returns the account id + */ + ClientAccountID(ctx) { - const minter = ctx.clientIdentity.getID(); + // Get ID of submitting client identity + const clientAccountID = ctx.clientIdentity.getID(); + return clientAccountID; + } + + /** + * 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) { - const amountInt = parseInt(amount); + // Get ID of submitting client identity + const clientAccountMSPID = ctx.clientIdentity.getMSPID(); + return clientAccountMSPID; + } - const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]); + /** + * 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 (this.isEmpty(nameBytes)) { + throw new Error('contract options need to be set before calling any function, call Initialize() to initialize contract'); + } + } - const currentBalanceBytes = await ctx.stub.getState(balanceKey); - if (!currentBalanceBytes || currentBalanceBytes.length === 0) { - throw new Error('The balance does not exist'); + /** + * 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'); } - const currentBalance = parseInt(currentBalanceBytes.toString()); - const updatedBalance = this.sub(currentBalance, amountInt); + } + + /** + * 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) { - await ctx.stub.putState(balanceKey, Buffer.from(updatedBalance.toString())); + // Convert value from string to int + const valueInt = parseInt(value); - // Decrease totalSupply - const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey); - if (!totalSupplyBytes || totalSupplyBytes.length === 0) { - throw new Error('totalSupply does not exist.'); + if (valueInt < 0) { // transfer of 0 is allowed in ERC20, so just validate against negative amounts + throw new Error('transfer amount cannot be negative'); } - const totalSupply = this.sub(parseInt(totalSupplyBytes.toString()), amountInt); + + 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: minter, to: '0x0', value: amountInt }; + const transferEvent = { from, to, value: valueInt }; 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 + * @param {String} owner The owner of tokens + * @param {String} spender The spender + * @param {Number} value The amount of token to be transferred */ - async ClientAccountBalance(ctx) { + async _approve(ctx, owner, spender, value) { + await this._approveEvent(ctx, owner, spender, value, true); + } - // Check contract options are already set first to execute the function - await this.CheckInitialized(ctx); + /** + * @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'); + } - // Get ID of submitting client identity - const clientAccountID = ctx.clientIdentity.getID(); + const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]); + await ctx.stub.putState(allowanceKey, Buffer.from(value.toString())); - 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`); + // 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))); } - 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) { + /** + * @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) { - // Check contract options are already set first to execute the function - await this.CheckInitialized(ctx); + // Retrieve the allowance of the spender + const currentAllowance = await this.Allowance(ctx, owner, spender); - // Get ID of submitting client identity - const clientAccountID = ctx.clientIdentity.getID(); - return clientAccountID; - } + // Convert value from string to int + const valueInt = parseInt(value); - // Checks that contract options have been already initialized - async CheckInitialized(ctx){ - const nameBytes = await ctx.stub.getState(nameKey); - if (!nameBytes || nameBytes.length === 0) { - throw new Error('contract options need to be set before calling any function, call Initialize() to initialize contract'); + // 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 diff --git a/token-erc-20/chaincode-javascript/test/tokenERC20.test.js b/token-erc-20/chaincode-javascript/test/tokenERC20.test.js index 133babb29e..433390ca04 100644 --- a/token-erc-20/chaincode-javascript/test/tokenERC20.test.js +++ b/token-erc-20/chaincode-javascript/test/tokenERC20.test.js @@ -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; - await token.Initialize(ctx, 'some name', 'some symbol', '2'); - - mockStub.putState.resolves('some state'); - mockStub.setEvent.returns('set event'); - - }); + mockClientIdentity.getMSPID.returns('Org1MSP'); - afterEach('Sandbox restoration', () => { - sandbox.restore(); + await token.Initialize(ctx, 'some name', 'some symbol', '2'); + 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'); + }); + }); + });