diff --git a/token-account-based/README.md b/token-account-based/README.md index bceb976a..ca260fba 100644 --- a/token-account-based/README.md +++ b/token-account-based/README.md @@ -30,10 +30,17 @@ The -ca flag is used to deploy the network using certificate authorities. This a ## Deploy the smart contract to the channel You can use the test network script to deploy the account-based token contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command: + +**For a Go Contract:** ``` ./network.sh deployCC -ccn token_account -ccp ../token-account-based/chaincode-go/ ``` +**For a JavaScript Contract:** +``` +./network.sh deployCC -ccn token_account -ccp ../token-account-based/chaincode-javascript/ -ccl javascript +``` + The above command deploys the go chaincode with short name `token_account`. The smart contract will use the default endorsement policy of majority of channel members. Since the channel has two members, this implies that we'll need to get peer endorsements from 2 out of the 2 channel members. @@ -140,6 +147,8 @@ Using the Org2 terminal, the Org2 recipient user can retrieve their own account peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountID","Args":[]}' ``` +**For a Go Contract:** + The function returns of recipient's account ID: ``` eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw== @@ -155,12 +164,28 @@ The result shows that the subject and issuer is indeed the recipient user from O x509::CN=recipient,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org2.example.com,O=org2.example.com,L=Hursley,ST=Hampshire,C=UK ``` +**For a JavaScript Contract:** + +The function returns of recipient's client ID. +The result shows that the subject and issuer is indeed the recipient user from Org2: +``` +x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com +``` + After the Org2 recipient provides their account ID to the minter, the minter can initiate a transfer from their account to the recipient's account. Back in the Org1 terminal, request the transfer of 100 tokens to the recipient account: + +**For a Go Contract:** ``` peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Transfer","Args":[ "eDUwOTo6Q049cmVjaXBpZW50LE9VPWNsaWVudCxPPUh5cGVybGVkZ2VyLFNUPU5vcnRoIENhcm9saW5hLEM9VVM6OkNOPWNhLm9yZzIuZXhhbXBsZS5jb20sTz1vcmcyLmV4YW1wbGUuY29tLEw9SHVyc2xleSxTVD1IYW1wc2hpcmUsQz1VSw==","100"]}' ``` +**For a JavaScript Contract:** +``` +export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Transfer","Args":[ "'"$RECIPIENT"'","100"]}' +``` + The `Transfer` function validates that the account associated with the calling client ID has sufficient funds for the transfer. It will then debit the caller's account and credit the recipient's account. Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist. @@ -186,6 +211,131 @@ The function queries the balance of the account associated with the recipient cl Congratulations, you've transferred 100 tokens! The Org2 recipient can now transfer tokens to other registered users in the same manner. +## Another scenario (for JavaScript contract only) + +This sample has another transfer method called `transferFrom`, which allows an approved spender to transfer fungible tokens on behalf of the account owner. The second scenario demonstrates how to approve the spender and transfer fungible tokens. + +In this tutorial, you will approve the spender and transfer tokens as follows: + +- A minter has already created tokens according to the scenario above. +- The same minter client uses the `approve` function to set the allowance of tokens a spender client can transfer on behalf of the minter. It is assumed that the spender has provided their client ID to the `approve` caller out of band. +- The spender client will then use the `transferFrom` function to transfer the requested number of tokens to the recipient's account on behalf of the minter. It is assumed that the recipient has provided their client ID to the `transferFrom` caller out of band. + +## Register identities + +You have already brought up the network and deployed the smart contract to the channel. We will use the same network and smart contract. + +We will use the Org1 CA to create the spender identity. +Back in the Org1 terminal, you can register a new spender client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org1 --id.name spender --id.secret spenderpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: +``` +fabric-ca-client enroll -u https://spender:spenderpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/spender@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the spender identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/spender@org1.example.com/msp/config.yaml +``` + +## Approve a spender + +The minter intends to approve 500 tokens to be transferred by the spender, but first the spender needs to provide their own client ID as the payment address. + +Open a 3rd terminal to represent the spender in Org1 and navigate to fabric-samples/test-network. Set the the environment variables for the Org1 spender user. + +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/spender@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:7051 +export TARGET_TLS_OPTIONS="-o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" +``` + +Now the Org1 spender can retrieve their own client ID: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountID","Args":[]}' +``` + +The function returns of spender's client ID. +The result shows that the subject and issuer is indeed the recipient user from Org2: +``` +x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com +``` + +After the Org1 spender provides their client ID to the minter, the minter can approve a spender. +Back in the Org1 terminal, request the approval of 100 tokens to be withdrew by the spender.: +``` +export SPENDER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"Approve","Args":["'"$SPENDER"'", "500"]}' +``` + +The approve function added that the spender client can consume 500 tokens on behalf of the minter. We can check the spender client's allowance from the minter by calling the `allowance` function. + +Let's request the spender's allowance from the minter: +``` +export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +peer chaincode query -C mychannel -n token_account -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +``` + +The function queries the allowance associated with the spender client ID and returns: +``` +500 +``` + +## TransferFrom tokens + +The spender intends to transfer 100 tokens to the Org2 recipient on behalf of the minter. The spender has already got the minter client Id and the recipient client ID. + +Back in the 3rd terminal, request the transfer of 100 tokens to the recipient account: +``` +export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com" +peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_account -c '{"function":"TransferFrom","Args":[ "'"$MINTER"'", "'"$RECIPIENT"'", "100"]}' +``` + +The `TransferFrom` function has three args: sender, recipient, amount. The function validates that the account associated with the sender has sufficient funds for the transfer. The function also validates if the allowance associated with the calling client ID exceeds funds to be transferred. +It will then debit the sender's account and credit the recipient's account. It will also decrease the spender's allowance approved by the minter. Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist. + +While still in the 3rd terminal, let's request the minter's account balance again: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"BalanceOf","Args":["'"$MINTER"'"]}' +``` + +The function queries the balance of the account associated with the minter client ID and returns: +``` +4800 +``` + +While still in the 3rd terminal, let's request the spender's allowance from the minter again: +``` +export SPENDER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=spender::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com" +peer chaincode query -C mychannel -n token_account -c '{"function":"Allowance","Args":["'"$MINTER"'", "'"$SPENDER"'"]}' +``` + +The function queries the allowance associated with the spender client ID and returns: +``` +400 +``` + +And then using the Org2 terminal, let's request the recipient's balance: +``` +peer chaincode query -C mychannel -n token_account -c '{"function":"ClientAccountBalance","Args":[]}' +``` + +The function queries the balance of the account associated with the recipient client ID and returns: +``` +200 +``` + +Congratulations, you've transferred 100 tokens! The Org2 recipient can now transfer tokens to other registered users in the same manner. + ## Clean up When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created: diff --git a/token-account-based/chaincode-javascript/.editorconfig b/token-account-based/chaincode-javascript/.editorconfig new file mode 100755 index 00000000..75a13be2 --- /dev/null +++ b/token-account-based/chaincode-javascript/.editorconfig @@ -0,0 +1,16 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/token-account-based/chaincode-javascript/.eslintignore b/token-account-based/chaincode-javascript/.eslintignore new file mode 100644 index 00000000..15958470 --- /dev/null +++ b/token-account-based/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/token-account-based/chaincode-javascript/.eslintrc.js b/token-account-based/chaincode-javascript/.eslintrc.js new file mode 100644 index 00000000..8d99762d --- /dev/null +++ b/token-account-based/chaincode-javascript/.eslintrc.js @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + env: { + node: true, + es6: true, + mocha: true + }, + parserOptions: { + ecmaVersion: 8, + sourceType: 'script' + }, + extends: "eslint:recommended", + rules: { + indent: ['error', 4], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'no-unused-vars': ['error', { args: 'none' }], + 'no-console': 'off', + curly: 'error', + eqeqeq: 'error', + 'no-throw-literal': 'error', + strict: 'error', + 'no-var': 'error', + 'dot-notation': 'error', + 'no-tabs': 'error', + 'no-trailing-spaces': 'error', + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-with': 'error', + 'operator-linebreak': 'error', + yoda: 'error', + 'quote-props': ['error', 'as-needed'], + 'no-constant-condition': ["error", { "checkLoops": false }] + } +}; diff --git a/token-account-based/chaincode-javascript/.gitignore b/token-account-based/chaincode-javascript/.gitignore new file mode 100644 index 00000000..c84ff1db --- /dev/null +++ b/token-account-based/chaincode-javascript/.gitignore @@ -0,0 +1,78 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless diff --git a/token-account-based/chaincode-javascript/index.js b/token-account-based/chaincode-javascript/index.js new file mode 100644 index 00000000..1841d315 --- /dev/null +++ b/token-account-based/chaincode-javascript/index.js @@ -0,0 +1,10 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const tokenERC20Contract = require('./lib/tokenERC20.js'); + +module.exports.TokenERC20Contract = tokenERC20Contract; +module.exports.contracts = [tokenERC20Contract]; \ No newline at end of file diff --git a/token-account-based/chaincode-javascript/lib/tokenERC20.js b/token-account-based/chaincode-javascript/lib/tokenERC20.js new file mode 100644 index 00000000..7ca67749 --- /dev/null +++ b/token-account-based/chaincode-javascript/lib/tokenERC20.js @@ -0,0 +1,409 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const { Contract } = require('fabric-contract-api'); + +// Define objectType names for prefix +const balancePrefix = 'balance'; +const allowancePrefix = 'allowance'; + +// Define key names for options +const nameKey = 'name'; +const symbolKey = 'symbol'; +const decimalsKey = 'decimals'; +const totalSupplyKey = 'totalSupply'; + +class TokenERC20Contract extends Contract { + + /** + * Return the name of the token - e.g. "MyToken". + * The original function name is `name` in ERC20 specification. + * However, 'name' conflicts with a parameter `name` in `Contract` class. + * As a work around, we use `TokenName` as an alternative function name. + * + * @param {Context} ctx the transaction context + * @returns {String} Returns the name of the token + */ + async TokenName(ctx) { + const nameBytes = await ctx.stub.getState(nameKey); + return nameBytes.toString(); + } + + /** + * Return the symbol of the token. E.g. “HIX”. + * + * @param {Context} ctx the transaction context + * @returns {String} Returns the symbol of the token + */ + async Symbol(ctx) { + const symbolBytes = await ctx.stub.getState(symbolKey); + return symbolBytes.toString(); + } + + /** + * Return the number of decimals the token uses + * e.g. 8, means to divide the token amount by 100000000 to get its user representation. + * + * @param {Context} ctx the transaction context + * @returns {Number} Returns the number of decimals + */ + async Decimals(ctx) { + const decimalsBytes = await ctx.stub.getState(decimalsKey); + const decimals = parseInt(decimalsBytes.toString()); + return decimals; + } + + /** + * Return the total token supply. + * + * @param {Context} ctx the transaction context + * @returns {Number} Returns the total token supply + */ + async TotalSupply(ctx) { + const totalSupplyBytes = await ctx.stub.getState(totalSupplyKey); + const totalSupply = parseInt(totalSupplyBytes.toString()); + return totalSupply; + } + + /** + * BalanceOf returns the balance of the given account. + * + * @param {Context} ctx the transaction context + * @param {String} owner The owner from which the balance will be retrieved + * @returns {Number} Returns the account balance + */ + async BalanceOf(ctx, owner) { + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [owner]); + + const balanceBytes = await ctx.stub.getState(balanceKey); + if (!balanceBytes || balanceBytes.length === 0) { + throw new Error(`the account ${owner} does not exist`); + } + const balance = parseInt(balanceBytes.toString()); + + return balance; + } + + /** + * Transfer transfers tokens from client account to recipient account. + * recipient account must be a valid clientID as returned by the ClientAccountID() function. + * + * @param {Context} ctx the transaction context + * @param {String} to The recipient + * @param {Integer} value The amount of token to be transferred + * @returns {Boolean} Return whether the transfer was successful or not + */ + async Transfer(ctx, to, value) { + const from = ctx.clientIdentity.getID(); + + const transferResp = await this._transfer(ctx, from, to, value); + if (!transferResp) { + throw new Error('Failed to transfer'); + } + + // Emit the Transfer event + const transferEvent = { from, to, value: parseInt(value) }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + return true; + } + + /** + * Transfer `value` amount of tokens from `from` to `to`. + * + * @param {Context} ctx the transaction context + * @param {String} from The sender + * @param {String} to The recipient + * @param {Integer} value The amount of token to be transferred + * @returns {Boolean} Return whether the transfer was successful or not + */ + async TransferFrom(ctx, from, to, value) { + 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 = currentAllowance - valueInt; + await ctx.stub.putState(allowanceKey, Buffer.from(updatedAllowance.toString())); + console.log(`spender ${spender} allowance updated from ${currentAllowance} to ${updatedAllowance}`); + + // Emit the Transfer event + const transferEvent = { from, to, value: valueInt }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + console.log('transferFrom ended successfully'); + return true; + } + + async _transfer(ctx, from, to, value) { + + // Convert value from string to int + const valueInt = parseInt(value); + + if (valueInt < 0) { // transfer of 0 is allowed in ERC20, so just validate against negative amounts + throw new Error('transfer amount cannot be negative'); + } + + // Retrieve the current balance of the sender + const fromBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [from]); + const fromCurrentBalanceBytes = await ctx.stub.getState(fromBalanceKey); + + if (!fromCurrentBalanceBytes || fromCurrentBalanceBytes.length === 0) { + throw new Error(`client account ${from} has no balance`); + } + + const fromCurrentBalance = parseInt(fromCurrentBalanceBytes.toString()); + + // Check if the sender has enough tokens to spend. + if (fromCurrentBalance < valueInt) { + throw new Error(`client account ${from} has insufficient funds.`); + } + + // Retrieve the current balance of the recepient + const toBalanceKey = ctx.stub.createCompositeKey(balancePrefix, [to]); + const toCurrentBalanceBytes = await ctx.stub.getState(toBalanceKey); + + let toCurrentBalance; + // If recipient current balance doesn't yet exist, we'll create it with a current balance of 0 + if (!toCurrentBalanceBytes || toCurrentBalanceBytes.length === 0) { + toCurrentBalance = 0; + } else { + toCurrentBalance = parseInt(toCurrentBalanceBytes.toString()); + } + + // Update the balance + const fromUpdatedBalance = fromCurrentBalance - valueInt; + const toUpdatedBalance = toCurrentBalance + valueInt; + + await ctx.stub.putState(fromBalanceKey, Buffer.from(fromUpdatedBalance.toString())); + await ctx.stub.putState(toBalanceKey, Buffer.from(toUpdatedBalance.toString())); + + console.log(`client ${from} balance updated from ${fromCurrentBalance} to ${fromUpdatedBalance}`); + console.log(`recipient ${to} balance updated from ${toCurrentBalance} to ${toUpdatedBalance}`); + + return true; + } + + /** + * Allows `spender` to spend `value` amount of tokens from the owner. + * + * @param {Context} ctx the transaction context + * @param {String} spender The spender + * @param {Integer} value The amount of tokens to be approved for transfer + * @returns {Boolean} Return whether the approval was successful or not + */ + async Approve(ctx, spender, value) { + const owner = ctx.clientIdentity.getID(); + + const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]); + + let valueInt = parseInt(value); + await ctx.stub.putState(allowanceKey, Buffer.from(valueInt.toString())); + + // Emit the Approval event + const approvalEvent = { owner, spender, value: valueInt }; + ctx.stub.setEvent('Approval', Buffer.from(JSON.stringify(approvalEvent))); + + console.log('approve ended successfully'); + return true; + } + + /** + * Returns the amount of tokens which `spender` is allowed to withdraw from `owner`. + * + * @param {Context} ctx the transaction context + * @param {String} owner The owner of tokens + * @param {String} spender The spender who are able to transfer the tokens + * @returns {Number} Return the amount of remaining tokens allowed to spent + */ + async Allowance(ctx, owner, spender) { + const allowanceKey = ctx.stub.createCompositeKey(allowancePrefix, [owner, spender]); + + const allowanceBytes = await ctx.stub.getState(allowanceKey); + if (!allowanceBytes || allowanceBytes.length === 0) { + throw new Error(`spender ${spender} has no allowance from ${owner}`); + } + + const allowance = parseInt(allowanceBytes.toString()); + return allowance; + } + + // ================== Extended Functions ========================== + + /** + * Set optional infomation for a token. + * + * @param {Context} ctx the transaction context + * @param {String} name The name of the token + * @param {String} symbol The symbol of the token + * @param {String} decimals The decimals of the token + * @param {String} totalSupply The totalSupply of the token + */ + async SetOption(ctx, name, symbol, decimals) { + await ctx.stub.putState(nameKey, Buffer.from(name)); + await ctx.stub.putState(symbolKey, Buffer.from(symbol)); + await ctx.stub.putState(decimalsKey, Buffer.from(decimals)); + + console.log(`name: ${name}, symbol: ${symbol}, decimals: ${decimals}`); + return true; + } + + /** + * Mint creates new tokens and adds them to minter's account balance + * + * @param {Context} ctx the transaction context + * @param {Integer} amount amount of tokens to be minted + * @returns {Object} The balance + */ + async Mint(ctx, amount) { + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to mint new tokens + const clientMSPID = ctx.clientIdentity.getMSPID(); + if (clientMSPID !== 'Org1MSP') { + throw new Error('client is not authorized to mint new tokens'); + } + + // Get ID of submitting client identity + const minter = ctx.clientIdentity.getID(); + + const amountInt = parseInt(amount); + if (amountInt <= 0) { + throw new Error('mint amount must be a positive integer'); + } + + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]); + + const currentBalanceBytes = await ctx.stub.getState(balanceKey); + // If minter current balance doesn't yet exist, we'll create it with a current balance of 0 + let currentBalance; + if (!currentBalanceBytes || currentBalanceBytes.length === 0) { + currentBalance = 0; + } else { + currentBalance = parseInt(currentBalanceBytes.toString()); + } + const updatedBalance = 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 = totalSupply + amountInt; + await ctx.stub.putState(totalSupplyKey, Buffer.from(totalSupply.toString())); + + // Emit the Transfer event + const transferEvent = { from: '0x0', to: minter, value: amountInt }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + console.log(`minter account ${minter} balance updated from ${currentBalance} to ${updatedBalance}`); + return true; + } + + /** + * Burn redeem tokens from minter's account balance + * + * @param {Context} ctx the transaction context + * @param {Integer} amount amount of tokens to be burned + * @returns {Object} The balance + */ + async Burn(ctx, amount) { + + // Check minter authorization - this sample assumes Org1 is the central banker with privilege to burn tokens + const clientMSPID = ctx.clientIdentity.getMSPID(); + if (clientMSPID !== 'Org1MSP') { + throw new Error('client is not authorized to mint new tokens'); + } + + const minter = ctx.clientIdentity.getID(); + + const amountInt = parseInt(amount); + + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter]); + + const currentBalanceBytes = await ctx.stub.getState(balanceKey); + if (!currentBalanceBytes || currentBalanceBytes.length === 0) { + throw new Error('The balance does not exist'); + } + const currentBalance = parseInt(currentBalanceBytes.toString()); + const updatedBalance = 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 = parseInt(totalSupplyBytes.toString()) - amountInt; + await ctx.stub.putState(totalSupplyKey, Buffer.from(totalSupply.toString())); + + // Emit the Transfer event + const transferEvent = { from: minter, to: '0x0', value: amountInt }; + ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent))); + + console.log(`minter account ${minter} balance updated from ${currentBalance} to ${updatedBalance}`); + return true; + } + + /** + * ClientAccountBalance returns the balance of the requesting client's account. + * + * @param {Context} ctx the transaction context + * @returns {Number} Returns the account balance + */ + async ClientAccountBalance(ctx) { + // Get ID of submitting client identity + const clientAccountID = ctx.clientIdentity.getID(); + + const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [clientAccountID]); + const balanceBytes = await ctx.stub.getState(balanceKey); + if (!balanceBytes || balanceBytes.length === 0) { + throw new Error(`the account ${clientAccountID} does not exist`); + } + const balance = parseInt(balanceBytes.toString()); + + return balance; + } + + // ClientAccountID returns the id of the requesting client's account. + // In this implementation, the client account ID is the clientId itself. + // Users can use this function to get their own account id, which they can then give to others as the payment address + async ClientAccountID(ctx) { + // Get ID of submitting client identity + const clientAccountID = ctx.clientIdentity.getID(); + return clientAccountID; + } + +} + +module.exports = TokenERC20Contract; \ No newline at end of file diff --git a/token-account-based/chaincode-javascript/package.json b/token-account-based/chaincode-javascript/package.json new file mode 100644 index 00000000..e704f980 --- /dev/null +++ b/token-account-based/chaincode-javascript/package.json @@ -0,0 +1,51 @@ +{ + "name": "token-erc20", + "version": "0.0.1", + "description": "Token-ERC20 contract implemented in JavaScript", + "main": "index.js", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive", + "mocha": "mocha --recursive", + "start": "fabric-chaincode-node start" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "eslint": "^4.19.1", + "mocha": "^8.0.1", + "nyc": "^14.1.1", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**", + "index.js", + ".eslintrc.js" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": false, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/token-account-based/chaincode-javascript/test/tokenERC20.test.js b/token-account-based/chaincode-javascript/test/tokenERC20.test.js new file mode 100644 index 00000000..6cfafa58 --- /dev/null +++ b/token-account-based/chaincode-javascript/test/tokenERC20.test.js @@ -0,0 +1,271 @@ +/* +SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const { Context } = require('fabric-contract-api'); +const { ChaincodeStub, ClientIdentity } = require('fabric-shim'); + +const { TokenERC20Contract } = require('..'); + +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); +const expect = chai.expect; + +chai.should(); +chai.use(chaiAsPromised); + +describe('Chaincode', () => { + let sandbox; + let token; + let ctx; + let mockStub; + let mockClientIdentity; + + beforeEach('Sandbox creation', () => { + sandbox = sinon.createSandbox(); + token = new TokenERC20Contract('token-erc20'); + + ctx = sinon.createStubInstance(Context); + mockStub = sinon.createStubInstance(ChaincodeStub); + ctx.stub = mockStub; + mockClientIdentity = sinon.createStubInstance(ClientIdentity); + ctx.clientIdentity = mockClientIdentity; + + mockStub.putState.resolves('some state'); + mockStub.setEvent.returns('set event'); + + }); + + afterEach('Sandbox restoration', () => { + sandbox.restore(); + }); + + 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'); + }); + }); + + 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'); + }); + }); + + 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); + }); + }); + + 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); + }); + }); + + describe('#BalanceOf', () => { + it('should work', async () => { + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.resolves(Buffer.from('1000')); + + const response = await token.BalanceOf(ctx, 'Alice'); + expect(response).to.equals(1000); + }); + }); + + describe('#_transfer', () => { + 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.'); + }); + + it('should transfer to a new account when the sender has enough token', async () => { + mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000')); + + 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); + }); + + it('should transfer to the existing account when the sender has enough token', async () => { + mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice'); + mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000')); + + 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); + }); + + }); + + describe('#Transfer', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('Alice'); + sinon.stub(token, '_transfer').returns(true); + + const response = await token.Transfer(ctx, 'Bob', '1000'); + const event = { from: 'Alice', to: 'Bob', value: 1000 }; + sinon.assert.calledWith(mockStub.setEvent, 'Transfer', Buffer.from(JSON.stringify(event))); + expect(response).to.equals(true); + }); + }); + + describe('#TransferFrom', () => { + it('should fail when the spender is not allowed to spend the token', async () => { + mockClientIdentity.getID.returns('Charlie'); + + 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.'); + }); + + it('should transfer when the spender is allowed to spend the token', async () => { + mockClientIdentity.getID.returns('Charlie'); + + 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 }; + sinon.assert.calledWith(mockStub.setEvent, 'Transfer', Buffer.from(JSON.stringify(event))); + expect(response).to.equals(true); + }); + }); + + describe('#Approve', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('Dave'); + mockStub.createCompositeKey.returns('allowance_Dave_Eve'); + + const response = await token.Approve(ctx, 'Ellen', '1000'); + sinon.assert.calledWith(mockStub.putState, 'allowance_Dave_Eve', Buffer.from('1000')); + expect(response).to.equals(true); + }); + }); + + describe('#Allowance', () => { + it('should work', async () => { + mockStub.createCompositeKey.returns('allowance_Dave_Eve'); + mockStub.getState.resolves(Buffer.from('1000')); + + const response = await token.Allowance(ctx, 'Dave', 'Eve'); + expect(response).to.equals(1000); + }); + }); + + 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')); + 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')); + 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')); + expect(response).to.equals(true); + }); + + }); + + 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')); + expect(response).to.equals(true); + }); + }); + + describe('#ClientAccountBalance', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('Alice'); + mockStub.createCompositeKey.returns('balance_Alice'); + mockStub.getState.resolves(Buffer.from('1000')); + + const response = await token.ClientAccountBalance(ctx,); + expect(response).to.equals(1000); + }); + }); + + describe('#ClientAccountID', () => { + it('should work', async () => { + mockClientIdentity.getID.returns('x509::{subject DN}::{issuer DN}'); + + const response = await token.ClientAccountID(ctx); + sinon.assert.calledOnce(mockClientIdentity.getID); + expect(response).to.equals('x509::{subject DN}::{issuer DN}'); + }); + }); + +});