diff --git a/asset-transfer-ledger-queries/chaincode-javascript/.gitignore b/asset-transfer-ledger-queries/chaincode-javascript/.gitignore new file mode 100644 index 00000000..a00ca941 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/.gitignore @@ -0,0 +1,77 @@ +# +# 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/ + +# 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/asset-transfer-ledger-queries/chaincode-javascript/META-INF/statedb/couchdb/indexes/indexOwner.json b/asset-transfer-ledger-queries/chaincode-javascript/META-INF/statedb/couchdb/indexes/indexOwner.json new file mode 100644 index 00000000..305f0904 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/META-INF/statedb/couchdb/indexes/indexOwner.json @@ -0,0 +1 @@ +{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} diff --git a/asset-transfer-ledger-queries/chaincode-javascript/asset_transfer_ledger_chaincode.js b/asset-transfer-ledger-queries/chaincode-javascript/asset_transfer_ledger_chaincode.js new file mode 100644 index 00000000..f6b1ee6f --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/asset_transfer_ledger_chaincode.js @@ -0,0 +1,473 @@ +/* +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; +const shim = require('fabric-shim'); +const util = require('util'); + +let Chaincode = class { + async Init(stub) { + let ret = stub.getFunctionAndParameters(); + console.info(ret); + console.info('=========== Instantiated Asset Chaincode ==========='); + return shim.success(); + } + + async Invoke(stub) { + console.info('Transaction ID: ' + stub.getTxID()); + console.info(util.format('Args: %j', stub.getArgs())); + + let ret = stub.getFunctionAndParameters(); + console.info(ret); + + let method = this[ret.fcn]; + if (!method) { + console.log('no function of name:' + ret.fcn + ' found'); + throw new Error('Received unknown function ' + ret.fcn + ' invocation'); + } + try { + let payload = await method(stub, ret.params, this); + return shim.success(payload); + } catch (err) { + console.log(err); + return shim.error(err); + } + } + + // =============================================== + // createAsset - create a new Asset + // =============================================== + async createAsset(stub, args, thisClass) { + if (args.length != 4) { + throw new Error('Incorrect number of arguments. Expecting 4'); + } + // ==== Input sanitation ==== + console.info('--- start init Asset ---') + if (args[0].lenth <= 0) { + throw new Error('1st argument must be a non-empty string'); + } + if (args[1].lenth <= 0) { + throw new Error('2nd argument must be a non-empty string'); + } + if (args[2].lenth <= 0) { + throw new Error('3rd argument must be a non-empty string'); + } + if (args[3].lenth <= 0) { + throw new Error('4th argument must be a non-empty string'); + } + let assetName = args[0]; + let color = args[1].toLowerCase(); + let owner = args[3].toLowerCase(); + let size = parseInt(args[2]); + if (typeof size !== 'number') { + throw new Error('3rd argument must be a numeric string'); + } + + const assetExists = thisClass["assetExists"] + const exists = await assetExists(stub, assetName) + if (exists) { + throw new Error('') + } + + // ==== Create asset object and marshal to JSON ==== + let asset = {}; + asset.docType = 'asset'; + asset.name = assetName; + asset.color = color; + asset.size = size; + asset.owner = owner; + + // === Save asset to state === + await stub.putState(assetName, Buffer.from(JSON.stringify(asset))); + let indexName = 'color~name' + let colorNameIndexKey = await stub.createCompositeKey(indexName, [asset.color, asset.name]); + console.info(colorNameIndexKey); + // Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble. + // Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value + await stub.putState(colorNameIndexKey, Buffer.from('\u0000')); + // ==== asset saved and indexed. Return success ==== + console.info('- end init asset'); + } + + async assetExists(stub, assetName) { + // ==== Check if asset already exists ==== + let assetState = await stub.getState(assetName); + if ( !assetState || assetState.length === 0 ) { + return false; + } + return true + } + + // =============================================== + // ReadAsset - read a asset from chaincode state + // =============================================== + async ReadAsset(stub, args, thisClass) { + if (args.length != 1) { + throw new Error('Incorrect number of arguments. Expecting name of the asset to query'); + } + + let name = args[0]; + if (!name) { + throw new Error(' asset name must not be empty'); + } + let assetAsbytes = await stub.getState(name); //get the asset from chaincode state + if (!assetAsbytes.toString()) { + let jsonResp = {}; + jsonResp.Error = 'Asset does not exist: ' + name; + throw new Error(JSON.stringify(jsonResp)); + } + console.info('======================================='); + console.log(assetAsbytes.toString()); + console.info('======================================='); + return assetAsbytes; + } + + // ================================================== + // delete - remove a asset key/value pair from state + // ================================================== + async delete(stub, args, thisClass) { + if (args.length != 1) { + throw new Error('Incorrect number of arguments. Expecting name of the asset to delete'); + } + let assetName = args[0]; + if (!assetName) { + throw new Error('asset name must not be empty'); + } + + exists = await assetExists(stub, assetName) + if (!exists) { + throw new Error('') + } + + // to maintain the color~name index, we need to read the asset first and get its color + let valAsbytes = await stub.getState(assetName); //get the asset from chaincode state + let jsonResp = {}; + if (!valAsbytes) { + jsonResp.error = 'asset does not exist: ' + name; + throw new Error(jsonResp); + } + let assetJSON = {}; + try { + assetJSON = JSON.parse(valAsbytes.toString()); + } catch (err) { + jsonResp = {}; + jsonResp.error = 'Failed to decode JSON of: ' + assetName; + throw new Error(jsonResp); + } + + await stub.deleteState(assetName); //remove the asset from chaincode state + + // delete the index + let indexName = 'color~name'; + let colorNameIndexKey = stub.createCompositeKey(indexName, [assetJSON.color, assetJSON.name]); + if (!colorNameIndexKey) { + throw new Error(' Failed to create the createCompositeKey'); + } + // Delete index entry to state. + await stub.deleteState(colorNameIndexKey); + } + + // =========================================================== + // transfer a asset by setting a new owner name on the asset + // =========================================================== + async transferAsset(stub, args, thisClass) { + // 0 1 + // 'name', 'bob' + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting assetname and owner') + } + + let assetName = args[0]; + let newOwner = args[1].toLowerCase(); + console.info('- start transferAsset ', assetName, newOwner); + + let assetAsBytes = await stub.getState(assetName); + if (!assetAsBytes || !assetAsBytes.toString()) { + throw new Error('asset does not exist'); + } + let assetToTransfer = {}; + try { + assetToTransfer = JSON.parse(assetAsBytes.toString()); //unmarshal + } catch (err) { + let jsonResp = {}; + jsonResp.error = 'Failed to decode JSON of: ' + assetName; + throw new Error(jsonResp); + } + console.info(assetToTransfer); + assetToTransfer.owner = newOwner; //change the owner + + let assetJSONasBytes = Buffer.from(JSON.stringify(assetToTransfer)); + await stub.putState(assetName, assetJSONasBytes); //rewrite the asset + + console.info('- end transferAsset (success)'); + } + + // =========================================================================================== + // getAssetsByRange performs a range query based on the start and end keys provided. + + // Read-only function results are not typically submitted to ordering. If the read-only + // results are submitted to ordering, or if the query is used in an update transaction + // and submitted to ordering, then the committing peers will re-execute to guarantee that + // result sets are stable between endorsement time and commit time. The transaction is + // invalidated by the committing peers if the result set has changed between endorsement + // time and commit time. + // Therefore, range queries are a safe option for performing update transactions based on query results. + // =========================================================================================== + async getAssetsByRange(stub, args, thisClass) { + + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting 2'); + } + + let startKey = args[0]; + let endKey = args[1]; + + let resultsIterator = await stub.getStateByRange(startKey, endKey); + let method = thisClass['getAllResults']; + let results = await method(resultsIterator, false); + + return Buffer.from(JSON.stringify(results)); + } + + // ==== Example: GetStateByPartialCompositeKey/RangeQuery ========================================= + // transferAssetsBasedOnColor will transfer assets of a given color to a certain new owner. + // Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'. + // Committing peers will re-execute range queries to guarantee that result sets are stable + // between endorsement time and commit time. The transaction is invalidated by the + // committing peers if the result set has changed between endorsement time and commit time. + // Therefore, range queries are a safe option for performing update transactions based on query results. + // =========================================================================================== + async transferAssetsBasedOnColor(stub, args, thisClass) { + + // 0 1 + // 'color', 'bob' + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting color and owner'); + } + + let color = args[0]; + let newOwner = args[1].toLowerCase(); + console.info('- start transferAssetsBasedOnColor ', color, newOwner); + + // Query the color~name index by color + // This will execute a key range query on all keys starting with 'color' + let coloredAssetResultsIterator = await stub.getStateByPartialCompositeKey('color~name', [color]); + + let method = thisClass['transferAsset']; + // Iterate through result set and for each asset found, transfer to newOwner + while (true) { + let responseRange = await coloredAssetResultsIterator.next(); + if (!responseRange || !responseRange.value || !responseRange.value.key) { + return; + } + console.log(responseRange.value.key); + + // let value = res.value.value.toString('utf8'); + let objectType; + let attributes; + ({ + objectType, + attributes + } = await stub.splitCompositeKey(responseRange.value.key)); + + let returnedColor = attributes[0]; + let returnedAssetName = attributes[1]; + console.info(util.format('- found a asset from index:%s color:%s name:%s\n', objectType, returnedColor, returnedAssetName)); + + // Now call the transfer function for the found asset. + // Re-use the same function that is used to transfer individual assets + let response = await method(stub, [returnedAssetName, newOwner]); + } + + let responsePayload = util.format('Transferred %s assets to %s', color, newOwner); + console.info('- end transferAssetsBasedOnColor: ' + responsePayload); + } + + + // ===== Example: Parameterized rich query ================================================= + // queryAssetsByOwner queries for assets based on a passed in owner. + // This is an example of a parameterized query where the query logic is baked into the chaincode, + // and accepting a single query parameter (owner). + // Only available on state databases that support rich query (e.g. CouchDB) + // ========================================================================================= + async queryAssetsByOwner(stub, args, thisClass) { + // 0 + // 'bob' + if (args.length < 1) { + throw new Error('Incorrect number of arguments. Expecting owner name.') + } + + let owner = args[0].toLowerCase(); + let queryString = {}; + queryString.selector = {}; + queryString.selector.docType = 'asset'; + queryString.selector.owner = owner; + let method = thisClass['getQueryResultForQueryString']; + let queryResults = await method(stub, JSON.stringify(queryString), thisClass); + return queryResults; //shim.success(queryResults); + } + + // ===== Example: Ad hoc rich query ======================================================== + // queryAssets uses a query string to perform a query for assets. + // Query string matching state database syntax is passed in and executed as is. + // Supports ad hoc queries that can be defined at runtime by the client. + // If this is not desired, follow the queryAssetsForOwner example for parameterized queries. + // Only available on state databases that support rich query (e.g. CouchDB) + // ========================================================================================= + async queryAssets(stub, args, thisClass) { + // 0 + // 'queryString' + if (args.length < 1) { + throw new Error('Incorrect number of arguments. Expecting queryString'); + } + let queryString = args[0]; + if (!queryString) { + throw new Error('queryString must not be empty'); + } + let method = thisClass['getQueryResultForQueryString']; + let queryResults = await method(stub, queryString, thisClass); + return queryResults; + } + + async getAllResults(iterator, isHistory) { + let allResults = []; + while (true) { + let res = await iterator.next(); + + if (res.value && res.value.value.toString()) { + let jsonRes = {}; + console.log(res.value.value.toString('utf8')); + + if (isHistory && isHistory === true) { + jsonRes.TxId = res.value.tx_id; + jsonRes.Timestamp = res.value.timestamp; + jsonRes.IsDelete = res.value.is_delete.toString(); + try { + jsonRes.Value = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Value = res.value.value.toString('utf8'); + } + } else { + jsonRes.Key = res.value.key; + try { + jsonRes.Record = JSON.parse(res.value.value.toString('utf8')); + } catch (err) { + console.log(err); + jsonRes.Record = res.value.value.toString('utf8'); + } + } + allResults.push(jsonRes); + } + if (res.done) { + console.log('end of data'); + await iterator.close(); + console.info(allResults); + return allResults; + } + } + } + + // ========================================================================================= + // getQueryResultForQueryString executes the passed in query string. + // Result set is built and returned as a byte array containing the JSON results. + // ========================================================================================= + async getQueryResultForQueryString(stub, queryString, thisClass) { + + console.info('- getQueryResultForQueryString queryString:\n' + queryString) + let resultsIterator = await stub.getQueryResult(queryString); + let method = thisClass['getAllResults']; + + let results = await method(resultsIterator, false); + + return Buffer.from(JSON.stringify(results)); + } + + async getHistoryForAsset(stub, args, thisClass) { + + if (args.length < 1) { + throw new Error('Incorrect number of arguments. Expecting 1') + } + let assetName = args[0]; + console.info('- start getHistoryForAsset: %s\n', assetName); + + let resultsIterator = await stub.getHistoryForKey(assetName); + let method = thisClass['getAllResults']; + let results = await method(resultsIterator, true); + + return Buffer.from(JSON.stringify(results)); + } + + // ====== Pagination ========================================================================= + // Pagination provides a method to retrieve records with a defined pagesize and + // start point (bookmark). An empty string bookmark defines the first "page" of a query + // result. Paginated queries return a bookmark that can be used in + // the next query to retrieve the next page of results. Paginated queries extend + // rich queries and range queries to include a pagesize and bookmark. + // + // Two examples are provided in this example. The first is getAssetsByRangeWithPagination + // which executes a paginated range query. + // The second example is a paginated query for rich ad-hoc queries. + // ========================================================================================= + + // ====== Example: Pagination with Range Query =============================================== + // getAssetsByRangeWithPagination performs a range query based on the start & end key, + // page size and a bookmark. + // + // The number of fetched records will be equal to or lesser than the page size. + // Paginated range queries are only valid for read only transactions. + // =========================================================================================== + async getAssetsByRangeWithPagination(stub, args, thisClass) { + if (args.length < 2) { + throw new Error('Incorrect number of arguments. Expecting 2'); + } + const startKey = args[0]; + const endKey = args[1]; + + const pageSize = parseInt(args[2], 10); + const bookmark = args[3]; + + const { iterator, metadata } = await stub.getStateByRangeWithPagination(startKey, endKey, pageSize, bookmark); + const getAllResults = thisClass['getAllResults']; + const results = await getAllResults(iterator, false); + // use RecordsCount and Bookmark to keep consistency with the go sample + results.ResponseMetadata = { + RecordsCount: metadata.fetched_records_count, + Bookmark: metadata.bookmark, + }; + return Buffer.from(JSON.stringify(results)); + } + + // ========================================================================================= + // getQueryResultForQueryStringWithPagination executes the passed in query string with + // pagination info. Result set is built and returned as a byte array containing the JSON results. + // ========================================================================================= + async queryAssetsWithPagination(stub, args, thisClass) { + + // 0 + // "queryString" + if (args.length < 3) { + return shim.Error("Incorrect number of arguments. Expecting 3") + } + + const queryString = args[0]; + const pageSize = parseInt(args[1], 10); + const bookmark = args[2]; + + const { iterator, metadata } = await stub.getQueryResultWithPagination(queryString, pageSize, bookmark); + const getAllResults = thisClass['getAllResults']; + const results = await getAllResults(iterator, false); + // use RecordsCount and Bookmark to keep consistency with the go sample + results.ResponseMetadata = { + RecordsCount: metadata.fetched_records_count, + Bookmark: metadata.bookmark, + }; + + return Buffer.from(JSON.stringify(results)); + } +}; + +shim.start(new Chaincode()); \ No newline at end of file diff --git a/asset-transfer-ledger-queries/chaincode-javascript/package.json b/asset-transfer-ledger-queries/chaincode-javascript/package.json new file mode 100644 index 00000000..1add8161 --- /dev/null +++ b/asset-transfer-ledger-queries/chaincode-javascript/package.json @@ -0,0 +1,17 @@ +{ + "name": "assets", + "version": "1.0.0", + "description": "asset chaincode implemented in node.js", + "engines": { + "node": ">=8.4.0", + "npm": ">=5.3.0" + }, + "scripts": { + "start": "node asset_transfer_ledger_chaincode.js" + }, + "engine-strict": true, + "license": "Apache-2.0", + "dependencies": { + "fabric-shim": "^2.0.0" + } +}