diff --git a/app/platform/fabric/Proxy.ts b/app/platform/fabric/Proxy.ts index bd1f3e79b..0de018053 100644 --- a/app/platform/fabric/Proxy.ts +++ b/app/platform/fabric/Proxy.ts @@ -9,7 +9,11 @@ import { ExplorerError } from '../../common/ExplorerError'; import { explorerError } from '../../common/ExplorerMessage'; import * as FabricConst from './utils/FabricConst'; import { SyncPlatform } from './sync/SyncPlatform'; -import { convertValidationCode, jsonObjSize, SyncServices } from './sync/SyncService'; +import { + convertValidationCode, + jsonObjSize, + SyncServices +} from './sync/SyncService'; import * as sha from 'js-sha256'; import * as FabricUtils from './utils/FabricUtils'; @@ -112,7 +116,9 @@ export class Proxy { async getPeersStatus(network_id, channel_genesis_hash) { const client = await this.platform.getClient(network_id); const channel_name = client.getChannelNameByHash(channel_genesis_hash); - let orderersList = await client.fabricGateway.getActiveOrderersList(channel_name); + let orderersList = await client.fabricGateway.getActiveOrderersList( + channel_name + ); const nodes = await this.persistence .getMetricService() .getPeerList(network_id, channel_genesis_hash); @@ -128,10 +134,10 @@ export class Proxy { const peers = []; for (const node of nodes) { - node.status = ""; + node.status = ''; if (node.peer_type === 'PEER') { if (discover_results && discover_results.peers_by_org) { - node.status = "DOWN"; + node.status = 'DOWN'; const org = discover_results.peers_by_org[node.mspid]; if (org === undefined) { continue; @@ -141,7 +147,7 @@ export class Proxy { node.ledger_height_low = peer.ledgerHeight.low; node.ledger_height_high = peer.ledgerHeight.high; node.ledger_height_unsigned = peer.ledgerHeight.unsigned; - node.status = 'UP'; + node.status = 'UP'; } } } @@ -169,7 +175,7 @@ export class Proxy { } } } - peers.push(node); + peers.push(node); } } @@ -220,7 +226,7 @@ export class Proxy { /** * Returns the channel data with latest block time - * + * * @param {*} network_id * @returns * @memberof Proxy @@ -228,36 +234,56 @@ export class Proxy { async getChannelsInfo(network_id) { try { const client = this.platform.getClient(network_id); - const channels = await this.persistence.getCrudService().getChannelsInfo(network_id); + const channels = await this.persistence + .getCrudService() + .getChannelsInfo(network_id); const updatedChannels = []; - + for (const channel of channels) { const channel_genesis_hash = client.getChannelGenHash(channel.channelname); let agoBlockTimes = this.getLatestBlockTime(channel); - + let channel_members = await client.fabricGateway.queryEndorsersCommitter( + channel.channelname + ); + try { - const chainInfo = await client.fabricGateway.queryChainInfo(channel.channelname); - + const chainInfo = await client.fabricGateway.queryChainInfo( + channel.channelname + ); + if (chainInfo && chainInfo.height && chainInfo.height.low >= 0) { const totalBlocks = chainInfo.height.low; - - if (channel_genesis_hash && channel_genesis_hash === channel.channel_genesis_hash) { - updatedChannels.push({ ...channel, totalBlocks, agoBlockTimes }); + + if ( + channel_genesis_hash && + channel_genesis_hash === channel.channel_genesis_hash + ) { + updatedChannels.push({ + ...channel, + totalBlocks, + agoBlockTimes, + channel_members + }); } else { updatedChannels.push({ ...channel, totalBlocks }); } } else { - logger.warn(`Invalid chain information for channel: ${channel.channelname}`); + logger.warn( + `Invalid chain information for channel: ${channel.channelname}` + ); } } catch (error) { - logger.error(`Error querying chain information for channel: ${channel.channelname}`, error); + logger.error( + `Error querying chain information for channel: ${channel.channelname}`, + error + ); } } - + logger.debug('getChannelsInfo %j', updatedChannels); return updatedChannels; } catch (error) { - logger.error("Error querying channel information:", error); + logger.error('Error querying channel information:', error); return null; } } @@ -467,7 +493,11 @@ export class Proxy { * @returns * @memberof Proxy */ - async fetchDataByBlockNo(network_id: string, channel_genesis_hash: string, blockNo: number) { + async fetchDataByBlockNo( + network_id: string, + channel_genesis_hash: string, + blockNo: number + ) { return await this.dataByBlockNo(network_id, channel_genesis_hash, blockNo); } @@ -479,22 +509,32 @@ export class Proxy { * @returns * @memberof Proxy */ - async fetchDataByTxnId(network_id: string, channel_genesis_hash: string, txnId: string) { - const results = await this.persistence.getCrudService().getTransactionByID(network_id, channel_genesis_hash, txnId); + async fetchDataByTxnId( + network_id: string, + channel_genesis_hash: string, + txnId: string + ) { + const results = await this.persistence + .getCrudService() + .getTransactionByID(network_id, channel_genesis_hash, txnId); if (results == null) { return await this.queryTxFromLedger(network_id, channel_genesis_hash, txnId); } return results; } - async queryTxFromLedger(network_id: string, channel_genesis_hash: string, txnId: string) { - let syncPlatform = new SyncPlatform(this.persistence, null) + async queryTxFromLedger( + network_id: string, + channel_genesis_hash: string, + txnId: string + ) { + let syncPlatform = new SyncPlatform(this.persistence, null); let sync = new SyncServices(syncPlatform, this.persistence); const client = this.platform.getClient(network_id); const channel_name = client.getChannelNameByHash(channel_genesis_hash); try { const txn = await client.fabricGateway.queryTransaction(channel_name, txnId); - logger.info("Transaction details from query Transaction ", txn); + logger.info('Transaction details from query Transaction ', txn); if (txn) { //Formatting of transaction details const txObj = txn.transactionEnvelope; @@ -567,13 +607,12 @@ export class Proxy { readSet, writeSet, validation_code, - payload_proposal_hash, + payload_proposal_hash }; return transaction; } return txn; - } - catch (e) { + } catch (e) { logger.debug('No transaction found with this txn id >> ', e); } } @@ -587,15 +626,23 @@ export class Proxy { * @returns * @memberof Proxy */ - async fetchDataByBlockRange(network_id: string, channel_genesis_hash: string, startBlockNo: number, endBlockNo: number) { - let blockValue, blockArray = []; + async fetchDataByBlockRange( + network_id: string, + channel_genesis_hash: string, + startBlockNo: number, + endBlockNo: number + ) { + let blockValue, + blockArray = []; for (let index = startBlockNo; index <= endBlockNo; index++) { - blockValue = await this.dataByBlockNo(network_id, channel_genesis_hash, index); - if (blockValue != "response_payloads is null") { + blockValue = await this.dataByBlockNo( + network_id, + channel_genesis_hash, + index + ); + if (blockValue != 'response_payloads is null') { blockArray.push(blockValue); - } - else - break; + } else break; } if (blockArray.length > 0) { return blockArray; @@ -604,49 +651,36 @@ export class Proxy { } //Re-usable component to fetch data using block no and block range - async dataByBlockNo(network_id: string, channel_genesis_hash: string, blockNo: number) { + async dataByBlockNo( + network_id: string, + channel_genesis_hash: string, + blockNo: number + ) { const client = this.platform.getClient(network_id); const channel_name = client.getChannelNameByHash(channel_genesis_hash); //fetch data from postgress - const results = await this.persistence.getCrudService().getBlockByBlocknum(network_id, channel_genesis_hash, blockNo); + const results = await this.persistence + .getCrudService() + .getBlockByBlocknum(network_id, channel_genesis_hash, blockNo); if (results == null) { - const block = await this.getBlockByNumber(network_id, channel_genesis_hash, blockNo); - if (block != "response_payloads is null") { - logger.info("block details from gateway", block); - const first_tx = block.data.data[0]; - const header = first_tx.payload.header; - const createdt = await FabricUtils.getBlockTimeStamp( - header.channel_header.timestamp - ); - const blockhash = await FabricUtils.generateBlockHash(block.header); - //For transaction id - const txLen = block.data.data.length; - let txArray = []; - for (let txIndex = 0; txIndex < txLen; txIndex++) { - const txObj = block.data.data[txIndex]; - let txid = txObj.payload.header.channel_header.tx_id; - txArray.push(txid); - } - const blockData = { - channelname: channel_name, - blocknum: block.header.number.toString(), - datahash: block.header.data_hash.toString('hex'), - prehash: block.header.previous_hash.toString('hex'), - txcount: block.data.data.length, - createdt, - prev_blockhash: '', - blockhash, + const block = await this.getBlockByNumber( + network_id, + channel_genesis_hash, + blockNo + ); + if (block != 'response_payloads is null') { + logger.info('block details from gateway', block); + return await this.formatBlockData( + block, channel_genesis_hash, - blksize: jsonObjSize(block), - txhash: txArray - }; - return blockData; + channel_name + ); } return block; } return results; } - + /* * @param {*} contract_name * @returns @@ -654,16 +688,104 @@ export class Proxy { */ async getContractMetadata(network_id, contract_name, channel_genesis_hash) { const client = this.platform.getClient(network_id); - const channel_name = client.getChannelNameByHash(channel_genesis_hash); + const channel_name = client.getChannelNameByHash(channel_genesis_hash); let metadata; try { - metadata = await client.fabricGateway.queryContractMetadata(channel_name, contract_name, channel_genesis_hash); - } catch (e) { + metadata = await client.fabricGateway.queryContractMetadata( + channel_name, + contract_name, + channel_genesis_hash + ); + } catch (e) { logger.debug('getContractMetadata >> ', e); - } if (metadata) { + } + if (metadata) { return metadata; } logger.error('response_payloads is null'); return 'response_payloads is null'; } + + async fetchBlockByTxId(network_id, channel_genesis_hash, txnId) { + const client = this.platform.getClient(network_id); + const channel_name = client.getChannelNameByHash(channel_genesis_hash); + let block; + try { + block = await client.fabricGateway.queryBlockByTxId(channel_name, txnId); + } catch (error) { + logger.debug('getBlockByTxId ', error); + } + if (block) { + logger.info('block details from queryBlockByTxId ', block); + return await this.formatBlockData(block, channel_genesis_hash, channel_name); + } + logger.error('response_payloads is null'); + return null; + } + + async fetchBlockByHash(network_id, channel_genesis_hash, hash) { + const client = this.platform.getClient(network_id); + const channel_name = client.getChannelNameByHash(channel_genesis_hash); + let block; + try { + block = await client.fabricGateway.queryBlockByHash(channel_name, hash); + } catch (error) { + logger.debug('getBlockByTxId ', error); + } + if (block) { + logger.info('block details from queryBlockByHash ', block); + return await this.formatBlockData(block, channel_genesis_hash, channel_name); + } + logger.error('response_payloads is null'); + return null; + } + + async fetchEndorsersCommitter(network_id, channel_genesis_hash) { + const client = this.platform.getClient(network_id); + const channel_name = client.getChannelNameByHash(channel_genesis_hash); + let channel_members; + try { + channel_members = await client.fabricGateway.queryEndorsersCommitter( + channel_name + ); + } catch (err) { + logger.error('Failed to get the data from fabric-network : ', err); + } + if (channel_members) { + return channel_members; + } + logger.error('response_payloads is null'); + return null; + } + + async formatBlockData(block, channel_genesis_hash, channel_name) { + const first_tx = block.data.data[0]; + const header = first_tx.payload.header; + const createdt = await FabricUtils.getBlockTimeStamp( + header.channel_header.timestamp + ); + const blockhash = await FabricUtils.generateBlockHash(block.header); + + const txLen = block.data.data.length; + let txArray = []; + for (let txIndex = 0; txIndex < txLen; txIndex++) { + const txObj = block.data.data[txIndex]; + let txid = txObj.payload.header.channel_header.tx_id; + txArray.push(txid); + } + const blockData = { + channelname: channel_name, + blocknum: block.header.number.toString(), + datahash: block.header.data_hash.toString('hex'), + prehash: block.header.previous_hash.toString('hex'), + txcount: block.data.data.length, + createdt, + prev_blockhash: '', + blockhash, + channel_genesis_hash, + blksize: jsonObjSize(block), + txhash: txArray + }; + return blockData; + } } diff --git a/app/platform/fabric/gateway/FabricGateway.ts b/app/platform/fabric/gateway/FabricGateway.ts index aeb97671e..4bb3a779d 100644 --- a/app/platform/fabric/gateway/FabricGateway.ts +++ b/app/platform/fabric/gateway/FabricGateway.ts @@ -2,531 +2,595 @@ *SPDX-License-Identifier: Apache-2.0 */ - import { X509Identity, Wallets, Gateway } from 'fabric-network'; - import * as fabprotos from 'fabric-protos'; - import { Discoverer, DiscoveryService } from 'fabric-common'; - import concat from 'lodash/concat'; - import * as path from 'path'; - import { helper } from '../../../common/helper'; - import { explorerError } from '../../../common/ExplorerMessage'; - import { ExplorerError } from '../../../common/ExplorerError'; - - /* eslint-disable @typescript-eslint/no-var-requires */ - const { BlockDecoder, Client } = require('fabric-common'); - const FabricCAServices = require('fabric-ca-client'); - /* eslint-enable @typescript-eslint/no-var-requires */ - - const logger = helper.getLogger('FabricGateway'); - - export class FabricGateway { - fabricConfig: any; - config: any; - gateway: any; - wallet: any; - tlsEnable: boolean; - defaultChannelName: string; - fabricCaEnabled: boolean; - client: any; - clientTlsIdentity: X509Identity; - FSWALLET: string; - enableAuthentication: boolean; - asLocalhost: boolean; - ds: DiscoveryService; - dsTargets: Discoverer[]; - - - /** - * Creates an instance of FabricGateway. - * @param {FabricConfig} config - * @memberof FabricGateway - */ - constructor(fabricConfig) { - this.fabricConfig = fabricConfig; - this.config = this.fabricConfig.getConfig(); - this.gateway = null; - this.wallet = null; - this.tlsEnable = false; - this.defaultChannelName = null; - this.gateway = new Gateway(); - this.fabricCaEnabled = false; - this.client = null; - this.clientTlsIdentity = null; - this.FSWALLET = null; - this.enableAuthentication = false; - this.asLocalhost = false; - this.ds = null; - this.dsTargets = []; - } - - async initialize() { - this.fabricCaEnabled = this.fabricConfig.isFabricCaEnabled(); - this.tlsEnable = this.fabricConfig.getTls(); - this.enableAuthentication = this.fabricConfig.getEnableAuthentication(); - this.FSWALLET = 'wallet/' + this.fabricConfig.getNetworkId(); - - const explorerAdminId = this.fabricConfig.getAdminUser(); - if (!explorerAdminId) { - logger.error('Failed to get admin ID from configuration file'); - throw new ExplorerError(explorerError.ERROR_1010); - } - - const info = `Loading configuration ${this.config}`; - logger.debug(info.toUpperCase()); - - this.defaultChannelName = this.fabricConfig.getDefaultChannel(); - try { - // Create a new file system based wallet for managing identities. - const walletPath = path.join(process.cwd(), this.FSWALLET); - this.wallet = await Wallets.newFileSystemWallet(walletPath); - // Check to see if we've already enrolled the admin user. - const identity = await this.wallet.get(explorerAdminId); - if (identity) { - logger.debug( - `An identity for the admin user: ${explorerAdminId} already exists in the wallet` - ); - } else if (this.fabricCaEnabled) { - logger.info('CA enabled'); - - await this.enrollCaIdentity( - explorerAdminId, - this.fabricConfig.getAdminPassword() - ); - } else { - /* - * Identity credentials to be stored in the wallet - * Look for signedCert in first-network-connection.json - */ - - const signedCertPem = this.fabricConfig.getOrgSignedCertPem(); - const adminPrivateKeyPem = this.fabricConfig.getOrgAdminPrivateKeyPem(); - await this.enrollUserIdentity( - explorerAdminId, - signedCertPem, - adminPrivateKeyPem - ); - logger.info('1'); - } - - if (!this.tlsEnable) { - Client.setConfigSetting('discovery-protocol', 'grpc'); - } else { - Client.setConfigSetting('discovery-protocol', 'grpcs'); - } - logger.info('2'); - - // Set connection options; identity and wallet - this.asLocalhost = - String(Client.getConfigSetting('discovery-as-localhost', 'true')) === - 'true'; - logger.info('3'); - - const connectionOptions = { - identity: explorerAdminId, - wallet: this.wallet, - discovery: { - enabled: true, - asLocalhost: this.asLocalhost - }, - clientTlsIdentity: '' - }; - logger.info('4', connectionOptions); - - const mTlsIdLabel = this.fabricConfig.getClientTlsIdentity(); - if (mTlsIdLabel) { - logger.info('client TLS enabled'); - this.clientTlsIdentity = await this.wallet.get(mTlsIdLabel); - if (this.clientTlsIdentity !== undefined) { - connectionOptions.clientTlsIdentity = mTlsIdLabel; - } else { - throw new ExplorerError( - `Not found Identity ${mTlsIdLabel} in your wallet` - ); - } - } - logger.info('5', this.config); - - // Connect to gateway - await this.gateway.connect(this.config, connectionOptions); - } catch (error) { - logger.error( - `${explorerError.ERROR_1010}: ${JSON.stringify(error, null, 2)}` - ); - throw new ExplorerError(explorerError.ERROR_1010); - } - } - - getEnableAuthentication() { - return this.enableAuthentication; - } - - getDiscoveryProtocol() { - return Client.getConfigSetting('discovery-protocol'); - } - - getDefaultMspId() { - return this.fabricConfig.getMspId(); - } - - getTls() { - return this.tlsEnable; - } - - getConfig() { - return this.config; - } - - /** - * @private method - * - */ - async enrollUserIdentity(userName, signedCertPem, adminPrivateKeyPem) { - const identity = { - credentials: { - certificate: signedCertPem, - privateKey: adminPrivateKeyPem - }, - mspId: this.fabricConfig.getMspId(), - type: 'X.509' - }; - logger.info('enrollUserIdentity: userName :', userName); - await this.wallet.put(userName, identity); - return identity; - } - - /** - * @private method - * - */ - async enrollCaIdentity(id, secret) { - if (!this.fabricCaEnabled) { - logger.error('CA server is not configured'); - return null; - } - - try { - const caName = this.config.organizations[this.fabricConfig.getOrganization()] - .certificateAuthorities[0]; - const ca = new FabricCAServices( - this.config.certificateAuthorities[caName].url, - { - trustedRoots: this.fabricConfig.getTlsCACertsPem(caName), - verify: false - } - ); - - const enrollment = await ca.enroll({ - enrollmentID: this.fabricConfig.getCaAdminUser(), - enrollmentSecret: this.fabricConfig.getCaAdminPassword() - }); - - logger.info('>>>>>>>>>>>>>>>>>>>>>>>>> enrollment : ca admin'); - - const identity = { - credentials: { - certificate: enrollment.certificate, - privateKey: enrollment.key.toBytes() - }, - mspId: this.fabricConfig.getMspId(), - type: 'X.509' - }; - - // Import identity wallet - await this.wallet.put(this.fabricConfig.getCaAdminUser(), identity); - - const adminUser = await this.getUserContext( - this.fabricConfig.getCaAdminUser() - ); - await ca.register( - { - affiliation: this.fabricConfig.getAdminAffiliation(), - enrollmentID: id, - enrollmentSecret: secret, - role: 'admin' - }, - adminUser - ); - - const enrollmentBEAdmin = await ca.enroll({ - enrollmentID: id, - enrollmentSecret: secret - }); - - logger.info( - '>>>>>>>>>>>>>>>>>>>>>>>>> registration & enrollment : BE admin' - ); - - const identityBEAdmin = { - credentials: { - certificate: enrollmentBEAdmin.certificate, - privateKey: enrollmentBEAdmin.key.toBytes() - }, - mspId: this.fabricConfig.getMspId(), - type: 'X.509' - }; - await this.wallet.put(id, identityBEAdmin); - - logger.debug('Successfully get user enrolled and imported to wallet, ', id); - - return identityBEAdmin; - } catch (error) { - // TODO decide how to proceed if error - logger.error('Error instantiating FabricCAServices ', error); - return null; - } - } - - async getUserContext(user) { - const identity = await this.wallet.get(user); - if (!identity) { - logger.error('Not exist user :', user); - return null; - } - const provider = this.wallet.getProviderRegistry().getProvider(identity.type); - const userContext = await provider.getUserContext(identity, user); - return userContext; - } - - async getIdentityInfo(label) { - let identityInfo; - logger.info('Searching for an identity with label: ', label); - try { - const list = await this.wallet.list(); - identityInfo = list.filter(id => { - return id.label === label; - }); - } catch (error) { - logger.error(error); - } - return identityInfo; - } - - async queryChannels() { - const network = await this.gateway.getNetwork(this.defaultChannelName); - - // Get the contract from the network. - const contract = network.getContract('cscc'); - const result = await contract.evaluateTransaction('GetChannels'); - const resultJson = fabprotos.protos.ChannelQueryResponse.decode(result); - logger.debug('queryChannels', resultJson); - return resultJson; - } - - async queryBlock(channelName, blockNum) { - try { - const network = await this.gateway.getNetwork(this.defaultChannelName); - - // Get the contract from the network. - const contract = network.getContract('qscc'); - const resultByte = await contract.evaluateTransaction( - 'GetBlockByNumber', - channelName, - String(blockNum) - ); - const resultJson = BlockDecoder.decode(resultByte); - logger.debug('queryBlock', resultJson); - return resultJson; - } catch (error) { - logger.error( - `Failed to get block ${blockNum} from channel ${channelName} : `, - error - ); - return null; - } - } - - async queryInstantiatedChaincodes(channelName) { - logger.info('queryInstantiatedChaincodes', channelName); - const network = await this.gateway.getNetwork(channelName); - let contract = network.getContract('lscc'); - let result = await contract.evaluateTransaction('GetChaincodes'); - let resultJson = fabprotos.protos.ChaincodeQueryResponse.decode(result); - if (resultJson.chaincodes.length <= 0) { - resultJson = { chaincodes: [], toJSON: null }; - contract = network.getContract('_lifecycle'); - result = await contract.evaluateTransaction('QueryChaincodeDefinitions', ''); - const decodedReult = fabprotos.lifecycle.QueryChaincodeDefinitionsResult.decode( - result - ); - for (const cc of decodedReult.chaincode_definitions) { - resultJson.chaincodes = concat(resultJson.chaincodes, { - name: cc.name, - version: cc.version - }); - } - } - logger.debug('queryInstantiatedChaincodes', resultJson); - return resultJson; - } - - async queryChainInfo(channelName) { - try { - const network = await this.gateway.getNetwork(this.defaultChannelName); - - // Get the contract from the network. - const contract = network.getContract('qscc'); - const resultByte = await contract.evaluateTransaction( - 'GetChainInfo', - channelName - ); - const resultJson = fabprotos.common.BlockchainInfo.decode(resultByte); - logger.debug('queryChainInfo', resultJson); - return resultJson; - } catch (error) { - logger.error( - `Failed to get chain info from channel ${channelName} : `, - error - ); - return null; - } - } - - async setupDiscoveryRequest(channelName) { - try { - const network = await this.gateway.getNetwork(channelName); - const channel = network.getChannel(); - this.ds = new DiscoveryService('be discovery service', channel); - - const idx = this.gateway.identityContext; - // do the three steps - this.ds.build(idx); - this.ds.sign(idx); - } catch (error) { - logger.error('Failed to set up discovery service for channel', error); - this.ds = null; - } - } - - async getDiscoveryServiceTarget() { - const client = new Client('discovery client'); - if (this.clientTlsIdentity) { - logger.info('client TLS enabled'); - client.setTlsClientCertAndKey( - this.clientTlsIdentity.credentials.certificate, - this.clientTlsIdentity.credentials.privateKey - ); - } else { - client.setTlsClientCertAndKey(); - } - - const targets: Discoverer[] = []; - const mspID = this.config.client.organization; - for (const peer of this.config.organizations[mspID].peers) { - const discoverer = new Discoverer(`be discoverer ${peer}`, client, mspID); - const url = this.config.peers[peer].url; - const pem = this.fabricConfig.getPeerTlsCACertsPem(peer); - let grpcOpt = {}; - if ('grpcOptions' in this.config.peers[peer]) { - grpcOpt = this.config.peers[peer].grpcOptions; - } - const peer_endpoint = client.newEndpoint( - Object.assign(grpcOpt, { - url: url, - pem: pem - }) - ); - await discoverer.connect(peer_endpoint); - targets.push(discoverer); - } - return targets; - } - - async sendDiscoveryRequest() { - let result; - try { - logger.info('Sending discovery request...'); - await this.ds - .send({ - asLocalhost: this.asLocalhost, - requestTimeout: 5000, - refreshAge: 15000, - targets: this.dsTargets - }) - .then(() => { - logger.info('Succeeded to send discovery request'); - }) - .catch(error => { - if (error) { - logger.warn('Failed to send discovery request for channel', error); - this.ds.close(); - } - }); - - result = await this.ds.getDiscoveryResults(true); - } catch (error) { - logger.warn('Failed to send discovery request for channel', error); - if (this.ds) { - this.ds.close(); - this.ds = null; - } - result = null; - } - return result; - } - - async getDiscoveryResult(channelName) { - - await this.setupDiscoveryRequest(channelName); - - if (!this.dsTargets.length) { - this.dsTargets = await this.getDiscoveryServiceTarget(); - } - - if (this.ds && this.dsTargets.length) { - const result = await this.sendDiscoveryRequest(); - return result; - } - - return null; - } - - async getActiveOrderersList(channel_name) { - const network = await this.gateway.getNetwork(channel_name); - let orderers = []; - try { - for (let [orderer, ordererMetadata] of network.discoveryService.channel.committers) { - let ordererAtrributes = { - name: orderer, - connected: ordererMetadata.connected - } - orderers.push(ordererAtrributes); - } - return orderers; - } catch (error) { - logger.error(`Failed to get orderers list : `, error); - return orderers; - } - } - - async queryTransaction(channelName:string, txnId:string) { - try { - const network = await this.gateway.getNetwork(this.defaultChannelName); - // Get the contract from the network. - const contract = network.getContract('qscc'); - const resultByte = await contract.evaluateTransaction( - 'GetTransactionByID', - channelName, - txnId - ); - const resultJson = BlockDecoder.decodeTransaction(resultByte); - logger.debug('queryTransaction', resultJson); - return resultJson; - } catch (error) { - logger.error( - `Failed to get transaction ${txnId} from channel ${channelName} : `, - error - ); - return null; - } - } - async queryContractMetadata(channel_name, contract_name) { - const network = await this.gateway.getNetwork(channel_name); - // Get the contract from the network.        - const contract = network.getContract(contract_name); - // Get the contract metadata from the network. - const result = await contract.evaluateTransaction('org.hyperledger.fabric:GetMetadata'); - const metadata = JSON.parse(result.toString('utf8')); - logger.debug('queryContractMetadata', metadata) - return metadata; - } - } - \ No newline at end of file +import { X509Identity, Wallets, Gateway } from 'fabric-network'; +import * as fabprotos from 'fabric-protos'; +import { Discoverer, DiscoveryService } from 'fabric-common'; +import concat from 'lodash/concat'; +import * as path from 'path'; +import { helper } from '../../../common/helper'; +import { explorerError } from '../../../common/ExplorerMessage'; +import { ExplorerError } from '../../../common/ExplorerError'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { BlockDecoder, Client } = require('fabric-common'); +const FabricCAServices = require('fabric-ca-client'); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const logger = helper.getLogger('FabricGateway'); + +export class FabricGateway { + fabricConfig: any; + config: any; + gateway: any; + wallet: any; + tlsEnable: boolean; + defaultChannelName: string; + fabricCaEnabled: boolean; + client: any; + clientTlsIdentity: X509Identity; + FSWALLET: string; + enableAuthentication: boolean; + asLocalhost: boolean; + ds: DiscoveryService; + dsTargets: Discoverer[]; + + /** + * Creates an instance of FabricGateway. + * @param {FabricConfig} config + * @memberof FabricGateway + */ + constructor(fabricConfig) { + this.fabricConfig = fabricConfig; + this.config = this.fabricConfig.getConfig(); + this.gateway = null; + this.wallet = null; + this.tlsEnable = false; + this.defaultChannelName = null; + this.gateway = new Gateway(); + this.fabricCaEnabled = false; + this.client = null; + this.clientTlsIdentity = null; + this.FSWALLET = null; + this.enableAuthentication = false; + this.asLocalhost = false; + this.ds = null; + this.dsTargets = []; + } + + async initialize() { + this.fabricCaEnabled = this.fabricConfig.isFabricCaEnabled(); + this.tlsEnable = this.fabricConfig.getTls(); + this.enableAuthentication = this.fabricConfig.getEnableAuthentication(); + this.FSWALLET = 'wallet/' + this.fabricConfig.getNetworkId(); + + const explorerAdminId = this.fabricConfig.getAdminUser(); + if (!explorerAdminId) { + logger.error('Failed to get admin ID from configuration file'); + throw new ExplorerError(explorerError.ERROR_1010); + } + + const info = `Loading configuration ${this.config}`; + logger.debug(info.toUpperCase()); + + this.defaultChannelName = this.fabricConfig.getDefaultChannel(); + try { + // Create a new file system based wallet for managing identities. + const walletPath = path.join(process.cwd(), this.FSWALLET); + this.wallet = await Wallets.newFileSystemWallet(walletPath); + // Check to see if we've already enrolled the admin user. + const identity = await this.wallet.get(explorerAdminId); + if (identity) { + logger.debug( + `An identity for the admin user: ${explorerAdminId} already exists in the wallet` + ); + } else if (this.fabricCaEnabled) { + logger.info('CA enabled'); + + await this.enrollCaIdentity( + explorerAdminId, + this.fabricConfig.getAdminPassword() + ); + } else { + /* + * Identity credentials to be stored in the wallet + * Look for signedCert in first-network-connection.json + */ + + const signedCertPem = this.fabricConfig.getOrgSignedCertPem(); + const adminPrivateKeyPem = this.fabricConfig.getOrgAdminPrivateKeyPem(); + await this.enrollUserIdentity( + explorerAdminId, + signedCertPem, + adminPrivateKeyPem + ); + logger.info('1'); + } + + if (!this.tlsEnable) { + Client.setConfigSetting('discovery-protocol', 'grpc'); + } else { + Client.setConfigSetting('discovery-protocol', 'grpcs'); + } + logger.info('2'); + + // Set connection options; identity and wallet + this.asLocalhost = + String(Client.getConfigSetting('discovery-as-localhost', 'true')) === + 'true'; + logger.info('3'); + + const connectionOptions = { + identity: explorerAdminId, + wallet: this.wallet, + discovery: { + enabled: true, + asLocalhost: this.asLocalhost + }, + clientTlsIdentity: '' + }; + logger.info('4', connectionOptions); + + const mTlsIdLabel = this.fabricConfig.getClientTlsIdentity(); + if (mTlsIdLabel) { + logger.info('client TLS enabled'); + this.clientTlsIdentity = await this.wallet.get(mTlsIdLabel); + if (this.clientTlsIdentity !== undefined) { + connectionOptions.clientTlsIdentity = mTlsIdLabel; + } else { + throw new ExplorerError( + `Not found Identity ${mTlsIdLabel} in your wallet` + ); + } + } + logger.info('5', this.config); + + // Connect to gateway + await this.gateway.connect(this.config, connectionOptions); + } catch (error) { + logger.error( + `${explorerError.ERROR_1010}: ${JSON.stringify(error, null, 2)}` + ); + throw new ExplorerError(explorerError.ERROR_1010); + } + } + + getEnableAuthentication() { + return this.enableAuthentication; + } + + getDiscoveryProtocol() { + return Client.getConfigSetting('discovery-protocol'); + } + + getDefaultMspId() { + return this.fabricConfig.getMspId(); + } + + getTls() { + return this.tlsEnable; + } + + getConfig() { + return this.config; + } + + /** + * @private method + * + */ + async enrollUserIdentity(userName, signedCertPem, adminPrivateKeyPem) { + const identity = { + credentials: { + certificate: signedCertPem, + privateKey: adminPrivateKeyPem + }, + mspId: this.fabricConfig.getMspId(), + type: 'X.509' + }; + logger.info('enrollUserIdentity: userName :', userName); + await this.wallet.put(userName, identity); + return identity; + } + + /** + * @private method + * + */ + async enrollCaIdentity(id, secret) { + if (!this.fabricCaEnabled) { + logger.error('CA server is not configured'); + return null; + } + + try { + const caName = this.config.organizations[this.fabricConfig.getOrganization()] + .certificateAuthorities[0]; + const ca = new FabricCAServices( + this.config.certificateAuthorities[caName].url, + { + trustedRoots: this.fabricConfig.getTlsCACertsPem(caName), + verify: false + } + ); + + const enrollment = await ca.enroll({ + enrollmentID: this.fabricConfig.getCaAdminUser(), + enrollmentSecret: this.fabricConfig.getCaAdminPassword() + }); + + logger.info('>>>>>>>>>>>>>>>>>>>>>>>>> enrollment : ca admin'); + + const identity = { + credentials: { + certificate: enrollment.certificate, + privateKey: enrollment.key.toBytes() + }, + mspId: this.fabricConfig.getMspId(), + type: 'X.509' + }; + + // Import identity wallet + await this.wallet.put(this.fabricConfig.getCaAdminUser(), identity); + + const adminUser = await this.getUserContext( + this.fabricConfig.getCaAdminUser() + ); + await ca.register( + { + affiliation: this.fabricConfig.getAdminAffiliation(), + enrollmentID: id, + enrollmentSecret: secret, + role: 'admin' + }, + adminUser + ); + + const enrollmentBEAdmin = await ca.enroll({ + enrollmentID: id, + enrollmentSecret: secret + }); + + logger.info( + '>>>>>>>>>>>>>>>>>>>>>>>>> registration & enrollment : BE admin' + ); + + const identityBEAdmin = { + credentials: { + certificate: enrollmentBEAdmin.certificate, + privateKey: enrollmentBEAdmin.key.toBytes() + }, + mspId: this.fabricConfig.getMspId(), + type: 'X.509' + }; + await this.wallet.put(id, identityBEAdmin); + + logger.debug('Successfully get user enrolled and imported to wallet, ', id); + + return identityBEAdmin; + } catch (error) { + // TODO decide how to proceed if error + logger.error('Error instantiating FabricCAServices ', error); + return null; + } + } + + async getUserContext(user) { + const identity = await this.wallet.get(user); + if (!identity) { + logger.error('Not exist user :', user); + return null; + } + const provider = this.wallet.getProviderRegistry().getProvider(identity.type); + const userContext = await provider.getUserContext(identity, user); + return userContext; + } + + async getIdentityInfo(label) { + let identityInfo; + logger.info('Searching for an identity with label: ', label); + try { + const list = await this.wallet.list(); + identityInfo = list.filter(id => { + return id.label === label; + }); + } catch (error) { + logger.error(error); + } + return identityInfo; + } + + async queryChannels() { + const network = await this.gateway.getNetwork(this.defaultChannelName); + + // Get the contract from the network. + const contract = network.getContract('cscc'); + const result = await contract.evaluateTransaction('GetChannels'); + const resultJson = fabprotos.protos.ChannelQueryResponse.decode(result); + logger.debug('queryChannels', resultJson); + return resultJson; + } + + async queryBlock(channelName, blockNum) { + try { + const network = await this.gateway.getNetwork(this.defaultChannelName); + + // Get the contract from the network. + const contract = network.getContract('qscc'); + const resultByte = await contract.evaluateTransaction( + 'GetBlockByNumber', + channelName, + String(blockNum) + ); + const resultJson = BlockDecoder.decode(resultByte); + logger.debug('queryBlock', resultJson); + return resultJson; + } catch (error) { + logger.error( + `Failed to get block ${blockNum} from channel ${channelName} : `, + error + ); + return null; + } + } + + async queryInstantiatedChaincodes(channelName) { + logger.info('queryInstantiatedChaincodes', channelName); + const network = await this.gateway.getNetwork(channelName); + let contract = network.getContract('lscc'); + let result = await contract.evaluateTransaction('GetChaincodes'); + let resultJson = fabprotos.protos.ChaincodeQueryResponse.decode(result); + if (resultJson.chaincodes.length <= 0) { + resultJson = { chaincodes: [], toJSON: null }; + contract = network.getContract('_lifecycle'); + result = await contract.evaluateTransaction('QueryChaincodeDefinitions', ''); + const decodedReult = fabprotos.lifecycle.QueryChaincodeDefinitionsResult.decode( + result + ); + for (const cc of decodedReult.chaincode_definitions) { + resultJson.chaincodes = concat(resultJson.chaincodes, { + name: cc.name, + version: cc.version + }); + } + } + logger.debug('queryInstantiatedChaincodes', resultJson); + return resultJson; + } + + async queryChainInfo(channelName) { + try { + const network = await this.gateway.getNetwork(this.defaultChannelName); + + // Get the contract from the network. + const contract = network.getContract('qscc'); + const resultByte = await contract.evaluateTransaction( + 'GetChainInfo', + channelName + ); + const resultJson = fabprotos.common.BlockchainInfo.decode(resultByte); + logger.debug('queryChainInfo', resultJson); + return resultJson; + } catch (error) { + logger.error( + `Failed to get chain info from channel ${channelName} : `, + error + ); + return null; + } + } + + async setupDiscoveryRequest(channelName) { + try { + const network = await this.gateway.getNetwork(channelName); + const channel = network.getChannel(); + this.ds = new DiscoveryService('be discovery service', channel); + + const idx = this.gateway.identityContext; + // do the three steps + this.ds.build(idx); + this.ds.sign(idx); + } catch (error) { + logger.error('Failed to set up discovery service for channel', error); + this.ds = null; + } + } + + async getDiscoveryServiceTarget() { + const client = new Client('discovery client'); + if (this.clientTlsIdentity) { + logger.info('client TLS enabled'); + client.setTlsClientCertAndKey( + this.clientTlsIdentity.credentials.certificate, + this.clientTlsIdentity.credentials.privateKey + ); + } else { + client.setTlsClientCertAndKey(); + } + + const targets: Discoverer[] = []; + const mspID = this.config.client.organization; + for (const peer of this.config.organizations[mspID].peers) { + const discoverer = new Discoverer(`be discoverer ${peer}`, client, mspID); + const url = this.config.peers[peer].url; + const pem = this.fabricConfig.getPeerTlsCACertsPem(peer); + let grpcOpt = {}; + if ('grpcOptions' in this.config.peers[peer]) { + grpcOpt = this.config.peers[peer].grpcOptions; + } + const peer_endpoint = client.newEndpoint( + Object.assign(grpcOpt, { + url: url, + pem: pem + }) + ); + await discoverer.connect(peer_endpoint); + targets.push(discoverer); + } + return targets; + } + + async sendDiscoveryRequest() { + let result; + try { + logger.info('Sending discovery request...'); + await this.ds + .send({ + asLocalhost: this.asLocalhost, + requestTimeout: 5000, + refreshAge: 15000, + targets: this.dsTargets + }) + .then(() => { + logger.info('Succeeded to send discovery request'); + }) + .catch(error => { + if (error) { + logger.warn('Failed to send discovery request for channel', error); + this.ds.close(); + } + }); + + result = await this.ds.getDiscoveryResults(true); + } catch (error) { + logger.warn('Failed to send discovery request for channel', error); + if (this.ds) { + this.ds.close(); + this.ds = null; + } + result = null; + } + return result; + } + + async getDiscoveryResult(channelName) { + await this.setupDiscoveryRequest(channelName); + + if (!this.dsTargets.length) { + this.dsTargets = await this.getDiscoveryServiceTarget(); + } + + if (this.ds && this.dsTargets.length) { + const result = await this.sendDiscoveryRequest(); + return result; + } + + return null; + } + + async getActiveOrderersList(channel_name) { + const network = await this.gateway.getNetwork(channel_name); + let orderers = []; + try { + for (let [orderer, ordererMetadata] of network.discoveryService.channel + .committers) { + let ordererAtrributes = { + name: orderer, + connected: ordererMetadata.connected + }; + orderers.push(ordererAtrributes); + } + return orderers; + } catch (error) { + logger.error(`Failed to get orderers list : `, error); + return orderers; + } + } + + async queryTransaction(channelName: string, txnId: string) { + try { + const network = await this.gateway.getNetwork(this.defaultChannelName); + // Get the contract from the network. + const contract = network.getContract('qscc'); + const resultByte = await contract.evaluateTransaction( + 'GetTransactionByID', + channelName, + txnId + ); + const resultJson = BlockDecoder.decodeTransaction(resultByte); + logger.debug('queryTransaction', resultJson); + return resultJson; + } catch (error) { + logger.error( + `Failed to get transaction ${txnId} from channel ${channelName} : `, + error + ); + return null; + } + } + async queryContractMetadata(channel_name, contract_name) { + const network = await this.gateway.getNetwork(channel_name); + const contract = network.getContract(contract_name); + // Get the contract metadata from the network. + const result = await contract.evaluateTransaction( + 'org.hyperledger.fabric:GetMetadata' + ); + const metadata = JSON.parse(result.toString('utf8')); + logger.debug('queryContractMetadata', metadata); + return metadata; + } + + async queryBlockByTxId(channelName, txnId) { + try { + const network = await this.gateway.getNetwork(channelName); + const contract = network.getContract('qscc'); + const resultByte = await contract.evaluateTransaction( + 'GetBlockByTxID', + channelName, + txnId + ); + const resultJson = BlockDecoder.decode(resultByte); + logger.debug('queryBlockByTxnId', resultJson); + return resultJson; + } catch (error) { + logger.error( + `Failed to get Block ${txnId} from channel ${channelName} : `, + error + ); + return null; + } + } + async queryBlockByHash(channelName, block_hash) { + try { + const network = await this.gateway.getNetwork(channelName); + const contract = network.getContract('qscc'); + const resultByte = await contract.evaluateTransaction( + 'GetBlockByHash', + channelName, + Buffer.from(block_hash, 'hex') + ); + const resultJson = BlockDecoder.decode(resultByte); + logger.debug('queryBlockByHash', resultJson); + return resultJson; + } catch (error) { + logger.error( + `Failed to get Block ${block_hash} from channel ${channelName} : `, + error + ); + return null; + } + } + + async queryEndorsersCommitter(channelName) { + try { + const network = await this.gateway.getNetwork(channelName); + const channel = network.getChannel(); + + const eventService = channel.newEventService('listener'); + let endorsers = []; + let committers = []; + for (const element of eventService.channel.committers.keys()) { + committers.push(element); + } + for (const element of eventService.channel.endorsers.keys()) { + endorsers.push(element); + } + return { committers: committers, endorsers: endorsers }; + } catch (error) { + logger.error( + `Failed to get Committers and Endorsers from channel ${channelName} : `, + error + ); + return null; + } + } +} diff --git a/app/rest/platformroutes.ts b/app/rest/platformroutes.ts index e1857645a..edcd38391 100644 --- a/app/rest/platformroutes.ts +++ b/app/rest/platformroutes.ts @@ -4,8 +4,6 @@ import * as requtil from './requestutils'; - - /** * * @@ -189,52 +187,67 @@ export async function platformroutes( * GET /fetchDataByBlockNo * curl -i 'http://:/fetchDataByBlockNo//' */ - router.get('/fetchDataByBlockNo/:channel_genesis_hash/:blockNo', (req, res) => { - const blockNo = parseInt(req.params.blockNo); - const channel_genesis_hash = req.params.channel_genesis_hash; - if (!isNaN(blockNo) && channel_genesis_hash) { - proxy.fetchDataByBlockNo(req.network, channel_genesis_hash, blockNo).then((data: any) => { - if (data != "response_payloads is null") { - res.send({ status: 200, data: data }); + router.get( + '/fetchDataByBlockNo/:channel_genesis_hash/:blockNo', + (req, res) => { + const blockNo = parseInt(req.params.blockNo); + const channel_genesis_hash = req.params.channel_genesis_hash; + if (!isNaN(blockNo) && channel_genesis_hash) { + proxy + .fetchDataByBlockNo(req.network, channel_genesis_hash, blockNo) + .then((data: any) => { + if (data != 'response_payloads is null') { + res.send({ status: 200, data: data }); + } else { + res.send({ status: 404, data: 'Block not found' }); + } + }); } else { - res.send({ status: 404, data: "Block not found" }); + return requtil.invalidRequest(req, res); } - }); - } else { - return requtil.invalidRequest(req, res); - } - }); + } + ); /** * * * Blocks by block range * GET /fetchDataByBlockRange * curl -i 'http://:/fetchDataByBlockRange///' - */ - router.get('/fetchDataByBlockRange/:channel_genesis_hash/:startBlockNo/:endBlockNo', (req, res) => { - const startBlockNo = parseInt(req.params.startBlockNo); - const endBlockNo = parseInt(req.params.endBlockNo); - const channel_genesis_hash = req.params.channel_genesis_hash; - if ( - startBlockNo <= endBlockNo && - startBlockNo >= 0 && - endBlockNo >= 0 && - !isNaN(startBlockNo) && - !isNaN(endBlockNo) && - channel_genesis_hash - ) { - proxy.fetchDataByBlockRange(req.network, channel_genesis_hash, startBlockNo, endBlockNo).then((data: any) => { - if (data != "response_payloads is null") { - res.send({ status: 200, data: data }); - } else { - res.send({ status: 404, data: "Block(s) not found" }); - } - }); - } else { - return requtil.invalidRequest(req, res); - } - }); + */ + router.get( + '/fetchDataByBlockRange/:channel_genesis_hash/:startBlockNo/:endBlockNo', + (req, res) => { + const startBlockNo = parseInt(req.params.startBlockNo); + const endBlockNo = parseInt(req.params.endBlockNo); + const channel_genesis_hash = req.params.channel_genesis_hash; + if ( + startBlockNo <= endBlockNo && + startBlockNo >= 0 && + endBlockNo >= 0 && + !isNaN(startBlockNo) && + !isNaN(endBlockNo) && + channel_genesis_hash + ) { + proxy + .fetchDataByBlockRange( + req.network, + channel_genesis_hash, + startBlockNo, + endBlockNo + ) + .then((data: any) => { + if (data != 'response_payloads is null') { + res.send({ status: 200, data: data }); + } else { + res.send({ status: 404, data: 'Block(s) not found' }); + } + }); + } else { + return requtil.invalidRequest(req, res); + } + } + ); /** * * @@ -245,16 +258,17 @@ export async function platformroutes( router.get('/fetchDataByTxnId/:channel_genesis_hash/:txnId', (req, res) => { const txnId = req.params.txnId; const channel_genesis_hash = req.params.channel_genesis_hash; - proxy.fetchDataByTxnId(req.network, channel_genesis_hash, txnId).then((data: any) => { - if (data != null) { - res.send({ status: 200, data: data }); - } - else{ - res.send({ status: 404, data: "Transaction not found" }); - } - }); + proxy + .fetchDataByTxnId(req.network, channel_genesis_hash, txnId) + .then((data: any) => { + if (data != null) { + res.send({ status: 200, data: data }); + } else { + res.send({ status: 404, data: 'Transaction not found' }); + } + }); }); - + /** * Return channel metadata * GET /metadata @@ -264,12 +278,72 @@ export async function platformroutes( const chaincode = req.params.chaincode; const channel_genesis_hash = req.params.channel_genesis_hash; if (chaincode && channel_genesis_hash) { - proxy.getContractMetadata(req.network, chaincode, channel_genesis_hash).then((data: any) => { - res.send({ status: 200, data: data }); - }); + proxy + .getContractMetadata(req.network, chaincode, channel_genesis_hash) + .then((data: any) => { + res.send({ status: 200, data: data }); + }); } else { return requtil.invalidRequest(req, res); } }); + router.get('/fetchBlockByTxId/:channel_genesis_hash/:txnId', (req, res) => { + const txnId = req.params.txnId; + const channel_genesis_hash = req.params.channel_genesis_hash; + + if (txnId && channel_genesis_hash) { + proxy + .fetchBlockByTxId(req.network, channel_genesis_hash, txnId) + .then((data: any) => { + if (data) { + res.send({ status: 200, data: data }); + } else { + res.send({ status: 404, data: 'Transaction details not found' }); + } + }); + } else { + return requtil.invalidRequest(req, res); + } + }); + + router.get( + '/fetchBlockByHash/:channel_genesis_hash/:block_hash', + (req, res) => { + const blockHash = req.params.block_hash; + const channel_genesis_hash = req.params.channel_genesis_hash; + + if (blockHash && channel_genesis_hash) { + proxy + .fetchBlockByHash(req.network, channel_genesis_hash, blockHash) + .then((data: any) => { + if (data) { + res.send({ status: 200, data: data }); + } else { + res.send({ status: 404, data: 'Transaction details not found' }); + } + }); + } else { + return requtil.invalidRequest(req, res); + } + } + ); + + router.get('/fetchEndorsersCommitter/:channel_genesis_hash', (req, res) => { + const channel_genesis_hash = req.params.channel_genesis_hash; + + if (channel_genesis_hash) { + proxy + .fetchEndorsersCommitter(req.network, channel_genesis_hash) + .then((data: any) => { + if (data) { + res.send({ status: 200, data: data }); + } else { + res.send({ status: 404, data: 'Data not found' }); + } + }); + } else { + return requtil.invalidRequest(req, res); + } + }); } // End platformroutes()