Created
May 29, 2019 18:29
-
-
Save gad0lin/51384bfea1c207ccf016a0cad8e413b6 to your computer and use it in GitHub Desktop.
hyperledger channel.js troubleshooting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const {parse, stringify} = require('flatted/cjs'); | |
/* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
'use strict'; | |
const sdk_utils = require('./utils.js'); | |
const client_utils = require('./client-utils.js'); | |
const util = require('util'); | |
const path = require('path'); | |
const Peer = require('./Peer.js'); | |
const ChannelEventHub = require('./ChannelEventHub.js'); | |
const Orderer = require('./Orderer.js'); | |
const BlockDecoder = require('./BlockDecoder.js'); | |
const TransactionID = require('./TransactionID.js'); | |
const ProtoLoader = require('./ProtoLoader'); | |
const Long = require('long'); | |
const logger = sdk_utils.getLogger('Channel.js'); | |
const MSPManager = require('./msp/msp-manager.js'); | |
const Policy = require('./Policy.js'); | |
const Constants = require('./Constants.js'); | |
const CollectionConfig = require('./SideDB.js'); | |
const {Identity} = require('./msp/identity.js'); | |
const ChannelHelper = require('./utils/ChannelHelper'); | |
const _ccProto = ProtoLoader.load(__dirname + '/protos/peer/chaincode.proto').protos; | |
const _transProto = ProtoLoader.load(__dirname + '/protos/peer/transaction.proto').protos; | |
const _proposalProto = ProtoLoader.load(__dirname + '/protos/peer/proposal.proto').protos; | |
const _responseProto = ProtoLoader.load(__dirname + '/protos/peer/proposal_response.proto').protos; | |
const _queryProto = ProtoLoader.load(__dirname + '/protos/peer/query.proto').protos; | |
const _peerConfigurationProto = ProtoLoader.load(__dirname + '/protos/peer/configuration.proto').protos; | |
const _commonProto = ProtoLoader.load(__dirname + '/protos/common/common.proto').common; | |
const _configtxProto = ProtoLoader.load(__dirname + '/protos/common/configtx.proto').common; | |
const _policiesProto = ProtoLoader.load(__dirname + '/protos/common/policies.proto').common; | |
const _ledgerProto = ProtoLoader.load(__dirname + '/protos/common/ledger.proto').common; | |
const _commonConfigurationProto = ProtoLoader.load(__dirname + '/protos/common/configuration.proto').common; | |
const _ordererConfigurationProto = ProtoLoader.load(__dirname + '/protos/orderer/configuration.proto').orderer; | |
const _abProto = ProtoLoader.load(__dirname + '/protos/orderer/ab.proto').orderer; | |
const _mspConfigProto = ProtoLoader.load(__dirname + '/protos/msp/msp_config.proto').msp; | |
const _mspPrincipalProto = ProtoLoader.load(__dirname + '/protos/msp/msp_principal.proto').common; | |
const _identityProto = ProtoLoader.load(path.join(__dirname, '/protos/msp/identities.proto')).msp; | |
const _discoveryProto = ProtoLoader.load(__dirname + '/protos/discovery/protocol.proto').discovery; | |
const _gossipProto = ProtoLoader.load(__dirname + '/protos/gossip/message.proto').gossip; | |
const _collectionProto = ProtoLoader.load(__dirname + '/protos/common/collection.proto').common; | |
const ImplicitMetaPolicy_Rule = {0: 'ANY', 1: 'ALL', 2: 'MAJORITY'}; | |
const PEER_NOT_ASSIGNED_MSG = 'Peer with name "%s" not assigned to this channel'; | |
const ORDERER_NOT_ASSIGNED_MSG = 'Orderer with name "%s" not assigned to this channel'; | |
function logAndThrow(methodName, errorMessage) { | |
console.log('%s error %s', methodName, errorMessage); | |
throw new Error(errorMessage); | |
} | |
/** | |
* Channels provide data isolation for a set of participating organizations. | |
* <br><br> | |
* A Channel object captures the settings needed to interact with a fabric backend in the | |
* context of a channel. These settings including the list of participating organizations, | |
* represented by instances of Membership Service Providers (MSP), the list of endorsing peers, | |
* and an orderer. | |
* <br><br> | |
* A client application can use the Channel object to create new channels with the orderer, | |
* update an existing channel, send various channel-aware requests to the peers such as | |
* invoking chaincodes to process transactions or queries. | |
* <br><br> | |
* A Channel object is also responsible for verifying endorsement signatures in transaction | |
* proposal responses. A channel object must be initialized after it has been configured with | |
* the list of peers and orderers. The initialization sends a get configuration block request | |
* to the primary orderer to retrieve the configuration settings for this channel. | |
* | |
* @class | |
*/ | |
const Channel = class { | |
/** | |
* Returns a new instance of the class. This is a client-side-only call. To | |
* create a new channel in the fabric, call [createChannel()]{@link Client#createChannel}. | |
* | |
* @param {string} name - Name to identify the channel. This value is used | |
* as the identifier of the channel when making channel-aware requests | |
* with the fabric, such as invoking chaincodes to endorse transactions. | |
* The naming of channels is enforced by the ordering service and must | |
* be unique within the fabric backend. Channel name in fabric network | |
* is subject to a pattern revealed in the configuration setting | |
* <code>channel-name-regx-checker</code>. | |
* @param {Client} clientContext - The client instance, which provides | |
* operational context such as the signing identity | |
*/ | |
constructor(name, clientContext) { | |
if (!name) { | |
throw new Error('Failed to create Channel. Missing requirement "name" parameter.'); | |
} | |
if (typeof name !== 'string') { | |
throw new Error('Failed to create Channel. channel name should be a string'); | |
} | |
const channelNameRegxChecker = sdk_utils.getConfigSetting('channel-name-regx-checker'); | |
if (channelNameRegxChecker) { | |
const {pattern, flags} = channelNameRegxChecker; | |
const namePattern = new RegExp(pattern ? pattern : '', flags ? flags : ''); | |
if (!(name.match(namePattern))) { | |
throw new Error(util.format('Failed to create Channel. channel name should match Regex %s, but got %j', namePattern, name)); | |
} | |
} | |
if (!clientContext) { | |
throw new Error('Failed to create Channel. Missing requirement "clientContext" parameter.'); | |
} | |
this._name = name; | |
this._channel_peers = new Map(); | |
this._anchor_peers = []; | |
this._orderers = new Map(); | |
this._kafka_brokers = []; | |
this._clientContext = clientContext; | |
this._msp_manager = new MSPManager(); | |
this._discovery_interests = new Map(); | |
this._discovery_results = null; | |
this._last_discover_timestamp = null; | |
this._discovery_peer = null; | |
this._use_discovery = sdk_utils.getConfigSetting('initialize-with-discovery', false); | |
this._as_localhost = sdk_utils.getConfigSetting('discovery-as-localhost', true); | |
this._endorsement_handler = null; // will be setup during initialization | |
this._commit_handler = null; | |
console.log('Constructed Channel instance: name - %s, network mode: %s', this._name, !this._devMode); | |
} | |
/** | |
* Close the service connections of all assigned peers and orderers | |
*/ | |
close() { | |
console.log('close - closing connections'); | |
this._channel_peers.forEach((channel_peer) => { | |
channel_peer.close(); | |
}); | |
this._orderers.forEach((orderer) => { | |
orderer.close(); | |
}); | |
} | |
/** | |
* @typedef {Object} InitializeRequest | |
* @property {string | Peer | ChannelPeer} target - Optional. The target peer to be used | |
* to make the initialization requests for configuration information. | |
* Default is to use the first ChannelPeer assigned to this channel. | |
* @property {boolean} discover - Optional. Use the discovery service on the | |
* the target peer to load the configuration and network information. | |
* Default is false. When false, the target peer will use the | |
* Peer query to load only the configuration information. | |
* @property {string} endorsementHandler - Optional. The path to a custom | |
* endorsement handler implementing {@link EndorsementHandler}. | |
* @property {string} commitHandler - Optional. The path to a custom | |
* commit handler implementing {@link CommitHandler}. | |
* @property {boolean} asLocalhost - Optional. Convert discovered host addresses | |
* to be 'localhost'. Will be needed when running a docker composed | |
* fabric network on the local system; otherwise should be disabled. Defaults to true. | |
* @property {byte[]} configUpdate - Optional. To initialize this channel with | |
* a serialized ConfigUpdate protobuf object. | |
*/ | |
/** | |
* Initializes the channel object with the Membership Service Providers (MSPs). The channel's | |
* MSPs are critical in providing applications the ability to validate certificates and verify | |
* signatures in messages received from the fabric backend. For instance, after calling | |
* [sendTransactionProposal()]{@link Channel#sendTransactionProposal}, the application can | |
* verify the signatures in the proposal response's endorsements to ensure they have not been | |
* tampered with. | |
* <br><br> | |
* This method retrieves the configuration from the orderer if no "config" parameter is passed in. | |
* Optionally a configuration may be passed in to initialize this channel without making the call | |
* to the orderer. | |
* | |
* @param {InitializeRequest} request - Optional. a {@link InitializeRequest} | |
* @return {Promise} A Promise that will resolve when the action is complete | |
*/ | |
async initialize(request) { | |
const method = 'initialize'; | |
console.log('%s - start', method); | |
let endorsement_handler_path = null; | |
let commit_handler_path = null; | |
if (request) { | |
if (request.configUpdate) { | |
console.log('%s - have a configupdate', method); | |
this.loadConfigUpdate(request.configUpdate); | |
return true; | |
} else { | |
if (typeof request.discover !== 'undefined') { | |
if (typeof request.discover === 'boolean') { | |
console.log('%s - user requested discover %s', method, request.discover); | |
this._use_discovery = request.discover; | |
} else { | |
throw new Error('Request parameter "discover" must be boolean'); | |
} | |
} | |
if (typeof request.asLocalhost !== 'undefined') { | |
if (typeof request.asLocalhost === 'boolean') { | |
console.log('%s - user requested discovery as localhost %s', method, request.asLocalhost); | |
this._as_localhost = request.asLocalhost; | |
} else { | |
throw new Error('Request parameter "asLocalhost" must be boolean'); | |
} | |
} | |
if (request.endorsementHandler) { | |
console.log('%s - user requested endorsementHandler %s', method, request.endorsementHandler); | |
endorsement_handler_path = request.endorsementHandler; | |
} | |
if (request.commitHandler) { | |
console.log('%s - user requested commitHandler %s', method, request.commitHandler); | |
commit_handler_path = request.commitHandler; | |
} | |
} | |
} | |
// setup the endorsement handler | |
if (!endorsement_handler_path && this._use_discovery) { | |
endorsement_handler_path = sdk_utils.getConfigSetting('endorsement-handler'); | |
console.log('%s - using config setting for endorsement handler ::%s', method, endorsement_handler_path); | |
} | |
if (endorsement_handler_path) { | |
this._endorsement_handler = require(endorsement_handler_path).create(this); | |
await this._endorsement_handler.initialize(); | |
} | |
// setup the commit handler | |
if (!commit_handler_path) { | |
commit_handler_path = sdk_utils.getConfigSetting('commit-handler'); | |
console.log('%s - using config setting for commit handler ::%s', method, commit_handler_path); | |
} | |
if (commit_handler_path) { | |
this._commit_handler = require(commit_handler_path).create(this); | |
await this._commit_handler.initialize(); | |
} | |
let results = null; | |
try { | |
results = await this._initialize(request); | |
} catch (error) { | |
console.log(' Problem with the initialize :: %s', error); | |
throw error; | |
} | |
return results; | |
} | |
async _initialize(request) { | |
console.log('hello world ') | |
const method = '_initialize'; | |
console.log('%s - start', method); | |
this._discovery_results = null; | |
this._last_discover_timestamp = null; | |
this._last_refresh_request = Object.assign({}, request); | |
let target_peer = this._discovery_peer; | |
if (request && request.target) { | |
target_peer = request.target; | |
} | |
if (this._use_discovery) { | |
console.log('%s - starting discovery', method); | |
try { | |
target_peer = this._getTargetForDiscovery(target_peer); | |
} catch (error) { | |
console.log('Problem getting a target peer for discovery service :: %s', error); | |
} | |
if (!target_peer) { | |
throw new Error('No target provided for discovery services'); | |
} | |
try { | |
let discover_request = { | |
target: target_peer, | |
config: true | |
}; | |
const discovery_results = await this._discover(discover_request); | |
if (discovery_results) { | |
if (discovery_results.msps) { | |
this._buildDiscoveryMSPs(discovery_results); | |
} else { | |
throw Error('No MSP information found'); | |
} | |
if (discovery_results.orderers) { | |
this._buildDiscoveryOrderers(discovery_results, discovery_results.msps, request); | |
} | |
if (discovery_results.peers_by_org) { | |
this._buildDiscoveryPeers(discovery_results, discovery_results.msps, request); | |
} | |
} | |
discovery_results.endorsement_plans = []; | |
const interests = []; | |
const plan_ids = []; | |
this._discovery_interests.forEach((interest, plan_id) => { | |
console.log('%s - have interest of:%s', method, plan_id); | |
plan_ids.push(plan_id); | |
interests.push(interest); | |
}); | |
for (const i in plan_ids) { | |
const plan_id = plan_ids[i]; | |
const interest = interests[i]; | |
discover_request = { | |
target: target_peer, | |
interests: [interest] | |
}; | |
let discover_interest_results = null; | |
try { | |
discover_interest_results = await this._discover(discover_request); | |
} catch (error) { | |
console.log('Not able to get an endorsement plan for %s', plan_id); | |
} | |
if (discover_interest_results && discover_interest_results.endorsement_plans && discover_interest_results.endorsement_plans[0]) { | |
const plan = this._buildDiscoveryEndorsementPlan(discover_interest_results, plan_id, discovery_results.msps, request); | |
discovery_results.endorsement_plans.push(plan); | |
console.log('Added an endorsement plan for %s', plan_id); | |
} else { | |
console.log('Not adding an endorsement plan for %s', plan_id); | |
} | |
} | |
discovery_results.timestamp = Date.now(); | |
this._discovery_results = discovery_results; | |
this._discovery_peer = target_peer; | |
this._last_discover_timestamp = discovery_results.timestamp; | |
return discovery_results; | |
} catch (error) { | |
console.log(error); | |
throw Error('Failed to discover ::' + error.toString()); | |
} | |
} else { | |
target_peer = this._getFirstAvailableTarget(target_peer); | |
const config_envelope = await this.getChannelConfig(target_peer); | |
console.log('initialize - got config envelope from getChannelConfig :: %j', config_envelope); | |
const config_items = this.loadConfigEnvelope(config_envelope); | |
return config_items; | |
} | |
} | |
_buildDiscoveryMSPs(discovery_results) { | |
const method = '_buildDiscoveryMSPs'; | |
console.log('%s - build msps', method); | |
for (const msp_name in discovery_results.msps) { | |
const msp = discovery_results.msps[msp_name]; | |
const config = { | |
rootCerts: msp.rootCerts, | |
intermediateCerts: msp.intermediateCerts, | |
admins: msp.admins, | |
cryptoSuite: this._clientContext._cryptoSuite, | |
id: msp.id, | |
orgs: msp.orgs, | |
tls_root_certs: msp.tls_root_certs, | |
tls_intermediate_certs: msp.tls_intermediate_certs | |
}; | |
this._msp_manager.addMSP(config); | |
} | |
} | |
_buildDiscoveryOrderers(discovery_results, msps, options) { | |
const method = '_buildDiscoveryOrderers'; | |
console.log('%s - build orderers', method); | |
for (const msp_id in discovery_results.orderers) { | |
console.log('%s - orderers msp:%s', method, msp_id); | |
const endpoints = discovery_results.orderers[msp_id].endpoints; | |
for (const endpoint of endpoints) { | |
console.log('%s - orderer mspid:%s endpoint:%s:%s', method, msp_id, endpoint.host, endpoint.port); | |
endpoint.name = this._buildOrdererName( | |
msp_id, | |
endpoint.host, | |
endpoint.port, | |
msps, | |
options | |
); | |
} | |
} | |
} | |
_buildDiscoveryPeers(discovery_results, msps, options) { | |
const method = '_buildDiscoveryPeers'; | |
console.log('%s - build peers', method); | |
for (const msp_id in discovery_results.peers_by_org) { | |
console.log('%s - peers msp:%s', method, msp_id); | |
const peers = discovery_results.peers_by_org[msp_id].peers; | |
for (const peer of peers) { | |
for (const chaincode of peer.chaincodes) { | |
const interest = this._buildDiscoveryInterest(chaincode.name); | |
const plan_id = JSON.stringify(interest); | |
console.log('%s - looking at adding plan_id of %s', method, plan_id); | |
this._discovery_interests.set(plan_id, interest); // will replace existing | |
console.log('%s - adding new interest of single chaincode ::%s', method, plan_id); | |
} | |
peer.name = this._buildPeerName( | |
peer.endpoint, | |
peer.mspid, | |
msps, | |
options | |
); | |
console.log('%s - peer:%j', method, peer); | |
} | |
} | |
} | |
_buildDiscoveryEndorsementPlan(discovery_results, plan_id, msps, options) { | |
const method = '_buildDiscoveryEndorsementPlan'; | |
console.log('%s - build endorsement plan for %s', method, plan_id); | |
const endorsement_plan = discovery_results.endorsement_plans[0]; | |
endorsement_plan.plan_id = plan_id; | |
for (const group_name in endorsement_plan.groups) { | |
console.log('%s - endorsing peer group %s', method, group_name); | |
const peers = endorsement_plan.groups[group_name].peers; | |
for (const peer of peers) { | |
peer.name = this._buildPeerName( | |
peer.endpoint, | |
peer.mspid, | |
msps, | |
options | |
); | |
console.log('%s - peer:%j', method, peer); | |
} | |
} | |
return endorsement_plan; | |
} | |
/** | |
* Get the channel name. | |
* @returns {string} The name of the channel. | |
*/ | |
getName() { | |
return this._name; | |
} | |
/** | |
* @typedef {Object} DiscoveryResultMSPConfig | |
* @property {string} rootCerts List of root certificates trusted by this MSP. | |
* They are used upon certificate validation. | |
* @property {string} intermediateCerts List of intermediate certificates | |
* trusted by this MSP. They are used upon certificate validation | |
* as follows: | |
* Validation attempts to build a path from the certificate to be | |
* validated (which is at one end of the path) and one of the certs | |
* in the RootCerts field (which is at the other end of the path). | |
* If the path is longer than 2, certificates in the middle are | |
* searched within the IntermediateCerts pool. | |
* @property {string} admins Identity denoting the administrator of this MSP | |
* @property {string} id the identifier of the MSP | |
* @property {string[]} orgs fabric organizational unit identifiers that | |
* belong to this MSP configuration | |
* @property {string} tls_root_certs TLS root certificates trusted by this MSP | |
* @property {string} tls_intermediate_certs TLS intermediate certificates | |
* trusted by this MSP | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResultEndpoints | |
* @property {DiscoveryResultEndpoint[]} endpoints | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResultEndpoint | |
* @property {string} host | |
* @property {number} port | |
* @property {string} name Optional. the name of this endpoint | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResultPeers | |
* @property {DiscoveryResultPeer[]} peers | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResultPeer | |
* @property {string} mspid | |
* @property {string} endpoint host:port for this peer | |
* @property {Long} ledger_height | |
* @property {string} name | |
* @property {DiscoveryResultChaincode[]} chaincodes | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResultChaincode | |
* @property {string} name | |
* @property {string} version | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResultEndorsementPlan | |
* @property {string} chaincode The chaincode name that is the first | |
* chaincode in the interest that was used to calculate this plan. | |
* @property {string} plan_id The string of the JSON object that represents | |
* the hint that was used to build the query for this result. The | |
* hint is a {@link DiscoveryChaincodeInterest} that contains chaincode | |
* names and collections that the discovery service uses to calculate | |
* the returned plan. | |
* @property {Object.<string, DiscoveryResultEndorsementGroup>} groups Specifies | |
* the endorsers, separated to groups. | |
* @property {DiscoveryResultEndorsementLayout[]} layouts Specifies options | |
* of fulfulling the endorsement policy | |
*/ | |
/** | |
* @typedef {Object.<string, number>} DiscoveryResultEndorsementLayout lists | |
* the group names, and the amount of signatures needed from each group. | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResultEndorsementGroup | |
* @property {DiscoveryResultPeer[]} peers the peers in this group | |
*/ | |
/** | |
* @typedef {Object} DiscoveryResults | |
* @property {Object.<string, DiscoveryResultMSPConfig>} msps - Optional. The msp config found. | |
* @property {Object.<string, DiscoveryResultEndpoints>} orderers - Optional. The orderers found. | |
* @property {Object.<string, DiscoveryResultPeers>} peers_by_org - Optional. The peers by org found. | |
* @property {DiscoveryResultEndorsementPlan[]} endorsement_plans - Optional. | |
* @property {number} timestamp - The timestamp at which the discovery results are updated. | |
*/ | |
/** | |
* @typedef {Object} DiscoveryChaincodeQuery | |
* Requests DiscoveryResults for a given list invocations. | |
* Each interest is a separate invocation of one or more chaincodes, | |
* which may include invocation on collections. | |
* The endorsement policy is evaluated independantly for each given | |
* interest. | |
* @property {DiscoveryChaincodeInterest[]} interests - defines interests | |
* in an invocations of chaincodes | |
* | |
* @example <caption>"chaincode and no collection"</caption> | |
* { | |
* interests: [ | |
* { chaincodes: [{ name: "mychaincode"}]} | |
* ] | |
* } | |
* | |
* @example <caption>"chaincode with collection"</caption> | |
* { | |
* interests: [ | |
* { chaincodes: [{ name: "mychaincode", collection_names: ["mycollection"] }]} | |
* ] | |
* } | |
* | |
* @example <caption>"chaincode to chaincode with collection"</caption> | |
* { | |
* interests: [ | |
* { chaincodes: [ | |
* { name: "mychaincode", collection_names: ["mycollection"] }}, | |
* { name: "myotherchaincode", collection_names: ["mycollection"] }} | |
* ] | |
* } | |
* ] | |
* } | |
* | |
* @example <caption>"query for multiple invocations"</caption> | |
* { | |
* interests: [ | |
* { chaincodes: [ | |
* { name: "mychaincode", collection_names: ["mycollection"] }}, | |
* { name: "myotherchaincode", collection_names: ["mycollection"] }} | |
* ] | |
* }, | |
* { chaincodes: [{ name: "mychaincode", collection_names: ["mycollection"] }]}, | |
* { chaincodes: [{ name: "mychaincode"}]} | |
* ] | |
* } | |
*/ | |
/** | |
* @typedef {Object} DiscoveryChaincodeInterest | |
* @property {DiscoveryChaincodeCall[]} chaincodes The chaincodes names and collections | |
* that will be sent to the discovery service to calculate an endorsement | |
* plan. | |
*/ | |
/** | |
* @typedef {Object} DiscoveryChaincodeCall | |
* @property {string} name - The name of the chaincode | |
* @property {string[]} collection_names - The names of the related collections | |
* @example <caption>"single chaincode"</caption> | |
* { name: "mychaincode"} | |
* | |
* @example <caption>"chaincode to chaincode"</caption> | |
* [ { name: "mychaincode"}, { name: "myotherchaincode"} ] | |
* | |
* @example <caption>"single chaincode with a collection"</caption> | |
* { name: "mychaincode", collection_names: ["mycollection"] } | |
* | |
* @example <caption>"chaincode to chaincode with a collection"</caption> | |
* [ | |
* { name: "mychaincode", collection_names: ["mycollection"] }, | |
* { name: "myotherchaincode", collection_names: ["mycollection"] }} | |
* ] | |
* | |
* @example <caption>"chaincode to chaincode with collections"</caption> | |
* [ | |
* { name: "mychaincode", collection_names: ["mycollection", "myothercollection"] }, | |
* { name: "myotherchaincode", collection_names: ["mycollection", "myothercollection"] }} | |
* ] | |
*/ | |
/** | |
* Return the discovery results. | |
* Discovery results are only available if this channel has been initialized. | |
* If the results are too old, they will be refreshed | |
* @param {DiscoveryChaincodeInterest[]} endorsement_hints - Indicate to discovery | |
* how to calculate the endorsement plans. | |
* @returns {Promise<DiscoveryResults>} | |
*/ | |
async getDiscoveryResults(endorsement_hints) { | |
const method = 'getDiscoveryResults'; | |
console.log('%s - start', method); | |
if (this._use_discovery) { | |
const have_new_interests = this._merge_hints(endorsement_hints); | |
const allowed_age = sdk_utils.getConfigSetting('discovery-cache-life', 300000); // default is 5 minutes | |
const now = Date.now(); | |
if (have_new_interests || now - this._last_discover_timestamp > allowed_age) { | |
console.log('%s - need to refresh :: have_new_interests %s', method, have_new_interests); | |
await this.refresh(); | |
} | |
console.log('%s - returning results', method); | |
return this._discovery_results; | |
} else { | |
console.log('No discovery results to return'); | |
// not working with discovery or we have not been initialized | |
throw new Error('This Channel has not been initialized or not initialized with discovery support'); | |
} | |
} | |
/** | |
* Return a single endorsement plan based off a {@link DiscoveryChaincodeInterest}. | |
* @param {DiscoveryChaincodeInterest} endorsement_hint - The chaincodes and | |
* collections of how the discovery service will calculate an endorsement plan. | |
* @return {DiscoveryResultEndorsementPlan} The endorsement plan based on the hint provided. | |
*/ | |
async getEndorsementPlan(endorsement_hint) { | |
const method = 'getEndorsementPlan'; | |
console.log('%s - start - %j', method, endorsement_hint); | |
let endorsement_plan = null; | |
const discovery_results = await this.getDiscoveryResults(endorsement_hint); | |
const plan_id = JSON.stringify(endorsement_hint); | |
console.log('%s - looking at plan_id of %s', method, plan_id); | |
if (discovery_results && discovery_results.endorsement_plans) { | |
for (const plan of discovery_results.endorsement_plans) { | |
if (plan.plan_id === plan_id) { | |
endorsement_plan = plan; | |
console.log('%s - found plan in known plans ::%s', method, plan_id); | |
break; | |
} | |
} | |
} | |
if (endorsement_plan) { | |
return JSON.parse(JSON.stringify(endorsement_plan)); | |
} else { | |
console.log('%s - plan not found in known plans', method, plan_id); | |
return null; | |
} | |
} | |
/** | |
* Refresh the channel's configuration. The MSP configurations, peers, | |
* orderers, and endorsement plans will be queired from the peer using | |
* the Discover Service. The queries will be made to the peer used previously | |
* for discovery if the 'target' parameter is not provided. | |
* | |
* @return {DiscoveryResults} - The results of refreshing | |
*/ | |
async refresh() { | |
const method = 'refresh'; | |
console.log('%s - using last initialize settings', method); | |
try { | |
const results = await this._initialize(this._last_refresh_request); | |
return results; | |
} catch (error) { | |
console.log('%s - failed:%s', method, error); | |
throw error; | |
} | |
} | |
/** | |
* @typedef {Object} OrganizationIdentifier | |
* @property {string} id The organization's MSP id | |
*/ | |
/** | |
* Get organization identifiers from the MSP's for this channel | |
* @returns {OrganizationIdentifier[]} Array of OrganizationIdentifier Objects | |
* representing the channel's participating organizations | |
*/ | |
getOrganizations() { | |
const method = 'getOrganizations'; | |
console.log('%s - start', method); | |
const msps = this._msp_manager.getMSPs(); | |
const mspIds = Object.keys(msps); | |
const orgs = mspIds.map((mspId) => { | |
return {id: mspId}; | |
}); | |
console.log('%s - orgs::%j', method, orgs); | |
return orgs; | |
} | |
/** | |
* Set the MSP Manager for this channel. This utility method will | |
* not normally be use as the [initialize()]{@link Channel#initialize} | |
* method will read this channel's current configuration and reset | |
* MSPManager with the MSP's found in the channel configuration. | |
* | |
* @param {MSPManager} msp_manager - The msp manager for this channel | |
*/ | |
setMSPManager(msp_manager) { | |
this._msp_manager = msp_manager; | |
} | |
/** | |
* Get the MSP Manager for this channel | |
* @returns {MSPManager} | |
*/ | |
getMSPManager() { | |
return this._msp_manager; | |
} | |
/** | |
* Add the peer object to the channel object. A channel object can be optionally | |
* configured with a list of peer objects, which will be used when calling certain | |
* methods such as [sendInstantiateProposal()]{@link Channel#sendInstantiateProposal}, | |
* [sendUpgradeProposal()]{@link Channel#sendUpgradeProposal}, | |
* [sendTransactionProposal]{@link Channel#sendTransactionProposal}. | |
* | |
* @param {Peer} peer - An instance of the Peer class that has been initialized with URL | |
* and other gRPC options such as TLS credentials and request timeout. | |
* @param {string} mspid - The mpsid of the organization this peer belongs. | |
* @param {ChannelPeerRoles} roles - Optional. The roles this peer will perform | |
* on this channel. A role that is not defined will default to true | |
* @param {boolean} replace - If a peer exist with the same name, replace | |
* with this one. | |
*/ | |
addPeer(peer, mspid, roles, replace) { | |
const name = peer.getName(); | |
console.log(' ++++++++++++ add peer ' + name + 'za1 ' + stringify(peer)); | |
const check = this._channel_peers.get(name); | |
if (check) { | |
if (replace) { | |
console.log('/n removing old peer --name: %s --URL: %s', peer.getName(), peer.getUrl()); | |
this.removePeer(check); | |
} else { | |
const error = new Error(); | |
error.name = 'DuplicatePeer'; | |
error.message = 'Peer ' + name + ' already exists'; | |
console.log(error.message); | |
throw error; | |
} | |
} | |
console.log('/n adding a new peer --name: %s --URL: %s', peer.getName(), peer.getUrl()); | |
const channel_peer = new ChannelPeer(mspid, this, peer, roles); | |
this._channel_peers.set(name, channel_peer); | |
} | |
/** | |
* Remove the peer object in the channel object's list of peers | |
* whose endpoint url property matches the url or name of the peer that is | |
* passed in. | |
* | |
* @param {Peer} peer - An instance of the Peer class. | |
*/ | |
removePeer(peer) { | |
this._channel_peers.delete(peer.getName()); | |
} | |
/** | |
* This method will return a {@link ChannelPeer} instance if assigned to this | |
* channel. Peers that have been created by the {@link Client#newPeer} | |
* method and then added to this channel may be reference by the url if no | |
* name was provided in the options during the create. | |
* A {@link ChannelPeer} provides a reference to peer and channel event hub along | |
* with how this peer is being used on this channel. | |
* | |
* @param {string} name - The name of the peer | |
* @returns {ChannelPeer} The ChannelPeer instance. | |
*/ | |
getPeer(name) { | |
console.log('etting peeaar' + name) | |
console.log(' channel peers ' + stringify(this._channel_peers)) | |
const channel_peer = this._channel_peers.get(name); | |
if (!channel_peer) { | |
throw new Error(util.format(PEER_NOT_ASSIGNED_MSG, name)); | |
} | |
return channel_peer; | |
} | |
/** | |
* This method will return a {@link ChannelPeer}. This object holds a reference | |
* to the {@link Peer} and the {@link ChannelEventHub} objects and the attributes | |
* of how the peer is defined on the channel. | |
* | |
* @param {string} name - The name of the peer assigned to this channel | |
* @returns {ChannelPeer} The ChannelPeer instance | |
*/ | |
getChannelPeer(name) { | |
const channel_peer = this._channel_peers.get(name); | |
console.log('getChannelPeer etting peeaar' + name) | |
console.log('getChannelPeer channel peers ' + stringify(this._channel_peers)) | |
if (!channel_peer) { | |
throw new Error(util.format(PEER_NOT_ASSIGNED_MSG, name)); | |
} | |
return channel_peer; | |
} | |
/** | |
* Returns a list of {@link ChannelPeer} assigned to this channel instance. | |
* A {@link ChannelPeer} provides a reference to peer and channel event hub along | |
* with how this peer is being used on this channel. | |
* @returns {ChannelPeer[]} The channel peer list on the channel. | |
*/ | |
getPeers() { | |
console.log('getPeers - list size: %s.', this._channel_peers.size); | |
const peers = []; | |
this._channel_peers.forEach((channel_peer) => { | |
peers.push(channel_peer); | |
}); | |
return peers; | |
} | |
/** | |
* Returns a list of {@link ChannelPeer} assigned to this channel instance. | |
* A {@link ChannelPeer} provides a reference to peer and channel event hub along | |
* with how this peer is being used on this channel. | |
* @returns {ChannelPeer[]} The channel peer list on the channel. | |
*/ | |
getChannelPeers() { | |
console.log('getChjannelPeers ' + stringify(this.getPeers())) | |
return this.getPeers(); | |
} | |
/** | |
* Add the orderer object to the channel object, this is a client-side-only operation. | |
* An application may add more than one orderer object to the channel object, however | |
* the SDK only uses the first one in the list to send broadcast messages to the | |
* orderer backend. | |
* | |
* @param {Orderer} orderer - An instance of the Orderer class. | |
* @param {boolean} replace - If an orderer exist with the same name, replace | |
* with this one. | |
*/ | |
addOrderer(orderer, replace) { | |
const name = orderer.getName(); | |
const check = this._orderers.get(name); | |
if (check) { | |
if (replace) { | |
this.removeOrderer(check); | |
} else { | |
const error = new Error(); | |
error.name = 'DuplicateOrderer'; | |
error.message = 'Orderer ' + name + ' already exists'; | |
console.log(error.message); | |
throw error; | |
} | |
} | |
this._orderers.set(name, orderer); | |
} | |
/** | |
* Remove the first orderer object in the channel object's list of orderers | |
* whose endpoint url property matches the url of the orderer that is | |
* passed in. | |
* | |
* @param {Orderer} orderer - An instance of the Orderer class. | |
*/ | |
removeOrderer(orderer) { | |
this._orderers.delete(orderer.getName()); | |
} | |
/** | |
* This method will return a {@link Orderer} instance if assigned to this | |
* channel. Peers that have been created by the {@link Client#newOrderer} | |
* method and then added to this channel may be reference by the url if no | |
* name was provided in the options during the create. | |
* | |
* @param {string} name - The name or url of the orderer | |
* @returns {Orderer} The Orderer instance. | |
*/ | |
getOrderer(name) { | |
const orderer = this._orderers.get(name); | |
if (!orderer) { | |
throw new Error(util.format(ORDERER_NOT_ASSIGNED_MSG, name)); | |
} | |
return orderer; | |
} | |
/** | |
* Returns the orderers of this channel object. | |
* @returns {Orderer[]} The list of orderers in the channel object | |
*/ | |
getOrderers() { | |
console.log('getOrderers - list size: %s.', this._orderers.size); | |
const orderers = []; | |
this._orderers.forEach((orderer) => { | |
orderers.push(orderer); | |
}); | |
return orderers; | |
} | |
/** | |
* Returns an {@link ChannelEventHub} object. An event hub object encapsulates the | |
* properties of an event stream on a peer node, through which the peer publishes | |
* notifications of blocks being committed in the channel's ledger. | |
* This method will create a new ChannelEventHub and not save a reference. | |
* Use the {getChannelEventHub} to reuse a ChannelEventHub. | |
* | |
* @param {Peer | string} peer A Peer instance or the name of a peer that has | |
* been assigned to the channel. | |
* @returns {ChannelEventHub} The ChannelEventHub instance | |
*/ | |
newChannelEventHub(peer) { | |
// Will always return one or throw | |
const peers = this._getTargets(peer, Constants.NetworkConfig.EVENT_SOURCE_ROLE, true); | |
const channel_event_hub = new ChannelEventHub(this, peers[0]); | |
return channel_event_hub; | |
} | |
/** | |
* Returns an {@link ChannelEventHub} object. An event hub object encapsulates the | |
* properties of an event stream on a peer node, through which the peer publishes | |
* notifications of blocks being committed in the channel's ledger. | |
* This method will create a new ChannelEventHub if one does not exist. | |
* | |
* @param {string} name - The peer name associated with this channel event hub. | |
* Use the {@link Peer#getName} method to get the name of a | |
* peer instance that has been added to this channel. | |
* @returns {ChannelEventHub} - The ChannelEventHub associated with the peer. | |
*/ | |
getChannelEventHub(name) { | |
if (!(typeof name === 'string')) { | |
throw new Error('"name" parameter must be a Peer name.'); | |
} | |
const _channel_peer = this._channel_peers.get(name); | |
console.log('event hub etting peeaar' + name) | |
console.log('event hub channel peers ' + stringify(this._channel_peers)) | |
if (!_channel_peer) { | |
throw new Error(util.format(PEER_NOT_ASSIGNED_MSG, name)); | |
} | |
return _channel_peer.getChannelEventHub(); | |
} | |
/** | |
* Returns a list of {@link ChannelEventHub} based on the peers that are | |
* defined in this channel that are in the organization. | |
* | |
* @param {string} mspid - Optional - The mspid of an organization | |
* @returns {ChannelEventHub[]} An array of ChannelEventHub instances | |
*/ | |
getChannelEventHubsForOrg(mspid) { | |
const method = 'getChannelEventHubsForOrg'; | |
let _mspid = null; | |
if (!mspid) { | |
_mspid = this._clientContext.getMspid(); | |
console.log('%s - starting - using client mspid: %s', method, _mspid); | |
} else { | |
_mspid = mspid; | |
console.log('%s - starting - mspid: %s', method, _mspid); | |
} | |
const channel_event_hubs = []; | |
this._channel_peers.forEach((channel_peer) => { | |
if (channel_peer.isInOrg(_mspid)) { | |
if (channel_peer.isInRole(Constants.NetworkConfig.EVENT_SOURCE_ROLE)) { | |
channel_event_hubs.push(channel_peer.getChannelEventHub()); | |
} else { | |
console.log('%s - channel peer:%s is not an event source', method, channel_peer.getName()); | |
} | |
} | |
}); | |
return channel_event_hubs; | |
} | |
/** | |
* Returns a list of {@link Peer} that are | |
* defined in this channel that are in the named organization. | |
* | |
* @param {string} mspid - Optional - The name of an organization | |
* @returns {Peer[]} An array of Peer instances | |
*/ | |
getPeersForOrg(mspid) { | |
const method = 'getPeersForOrg'; | |
let _mspid = null; | |
if (!mspid) { | |
_mspid = this._clientContext.getMspid(); | |
console.log('%s - starting - using client mspid: %s', method, _mspid); | |
} else { | |
_mspid = mspid; | |
console.log('%s - starting - mspid: %s', method, _mspid); | |
} | |
const peers = []; | |
this._channel_peers.forEach((channel_peer) => { | |
if (channel_peer.isInOrg(_mspid)) { | |
peers.push(channel_peer); | |
} | |
}); | |
return peers; | |
} | |
/** | |
* @typedef {Object} OrdererRequest | |
* @property {TransactionID} txId - Optional. Object with the transaction id and nonce | |
* @property {Orderer} orderer - Optional. The orderer instance or string name | |
* of the orderer to retrieve genesis block from | |
*/ | |
/** | |
* A channel's first block is called the "genesis block". This block captures the | |
* initial channel configuration. For a peer node to join the channel, it must be | |
* provided the genesis block. This method must be called before calling | |
* [joinChannel()]{@link Channel#joinChannel}. | |
* | |
* @param {OrdererRequest} request - Optional - A transaction ID object | |
* @returns {Promise} A Promise for an encoded protobuf "Block" | |
*/ | |
getGenesisBlock(request) { | |
console.log('getGenesisBlock - start'); | |
if (!request) { | |
request = {}; | |
} | |
// verify that we have an orderer configured | |
const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name); | |
let signer = null; | |
let tx_id = request.txId; | |
if (!tx_id) { | |
signer = this._clientContext._getSigningIdentity(true); | |
tx_id = new TransactionID(signer, true); | |
} else { | |
signer = this._clientContext._getSigningIdentity(tx_id.isAdmin()); | |
} | |
// now build the seek info, will be used once the channel is created | |
// to get the genesis block back | |
// build start | |
const seekSpecifiedStart = new _abProto.SeekSpecified(); | |
seekSpecifiedStart.setNumber(0); | |
const seekStart = new _abProto.SeekPosition(); | |
seekStart.setSpecified(seekSpecifiedStart); | |
// build stop | |
const seekSpecifiedStop = new _abProto.SeekSpecified(); | |
seekSpecifiedStop.setNumber(0); | |
const seekStop = new _abProto.SeekPosition(); | |
seekStop.setSpecified(seekSpecifiedStop); | |
// seek info with all parts | |
const seekInfo = new _abProto.SeekInfo(); | |
seekInfo.setStart(seekStart); | |
seekInfo.setStop(seekStop); | |
seekInfo.setBehavior(_abProto.SeekInfo.SeekBehavior.BLOCK_UNTIL_READY); | |
// build the header for use with the seekInfo payload | |
const seekInfoHeader = client_utils.buildChannelHeader( | |
_commonProto.HeaderType.DELIVER_SEEK_INFO, | |
this._name, | |
tx_id.getTransactionID(), | |
this._initial_epoch, | |
null, | |
client_utils.buildCurrentTimestamp(), | |
this._clientContext.getClientCertHash() | |
); | |
const seekHeader = client_utils.buildHeader(signer, seekInfoHeader, tx_id.getNonce()); | |
const seekPayload = new _commonProto.Payload(); | |
seekPayload.setHeader(seekHeader); | |
seekPayload.setData(seekInfo.toBuffer()); | |
// building manually or will get protobuf errors on send | |
const envelope = client_utils.toEnvelope(client_utils.signProposal(signer, seekPayload)); | |
return orderer.sendDeliver(envelope); | |
} | |
/* | |
* Internal use only | |
* | |
* @typedef {Object} DiscoveryRequest | |
* @property {Peer | string} target - Optional. A Peer object or Peer name that | |
* will be asked to discovery information about this channel. | |
* Default is the first peer assigned to this channel that has the | |
* 'discover' role. | |
* @property {DiscoveryChaincodeInterest[]} interests - Optional. An | |
* Array of {@link DiscoveryChaincodeInterest} that have chaincodes | |
* and collections to calculate the endorsement plans. | |
* @property {boolean} config - Optional. To indicate that the channel configuration | |
* should be included in the discovery query. | |
* @property {boolean} local - Optional. To indicate that the local endpoints | |
* should be included in the discovery query. | |
* @property {boolean} useAdmin - Optional. To indicate that the admin identity | |
* should be used to make the discovery request | |
*/ | |
/** | |
* Send a request to a known peer to discover information about the fabric | |
* network. | |
* | |
* @param {DiscoveryRequest} request - | |
* @returns {DiscoveryResponse} The results from the discovery service | |
*/ | |
async _discover(request) { | |
console.log(' heel _discover') | |
console.log(' heel _discover') | |
console.log(' heel _discover') | |
const method = '_discover'; | |
const self = this; | |
console.log('%s - start', method); | |
const results = {}; | |
if (!request) { | |
request = {}; | |
} | |
let useAdmin = true; // default | |
if (typeof request.useAdmin === 'boolean') { | |
useAdmin = request.useAdmin; | |
} | |
const target_peer = this._getTargetForDiscovery(request.target); | |
console.log(' target _ peer ' + target_peer) | |
const signer = this._clientContext._getSigningIdentity(useAdmin); // use the admin if assigned | |
console.log(' singer ' + stringify(signer)) | |
const discovery_request = new _discoveryProto.Request(); | |
const authentication = new _discoveryProto.AuthInfo(); | |
authentication.setClientIdentity(signer.serialize()); | |
const cert_hash = this._clientContext.getClientCertHash(true); | |
if (cert_hash) { | |
authentication.setClientTlsCertHash(cert_hash); | |
} | |
discovery_request.setAuthentication(authentication); | |
// be sure to add all entries to this array before setting into the | |
// grpc object | |
const queries = []; | |
// if doing local it will be index 0 of the results | |
if (request.local) { | |
console.log('reqruire local') | |
const query = new _discoveryProto.Query(); | |
queries.push(query); | |
const local_peers = new _discoveryProto.LocalPeerQuery(); | |
query.setLocalPeers(local_peers); | |
console.log(stringify(query)) | |
console.log('%s - adding local peers query', method); | |
} | |
if (request.config) { | |
console.log('reqruire config ') | |
let query = new _discoveryProto.Query(); | |
queries.push(query); | |
query.setChannel(this.getName()); | |
const config_query = new _discoveryProto.ConfigQuery(); | |
query.setConfigQuery(config_query); | |
console.log('%s - adding config query', method); | |
query = new _discoveryProto.Query(); | |
queries.push(query); | |
query.setChannel(this.getName()); | |
const peer_query = new _discoveryProto.PeerMembershipQuery(); | |
query.setPeerQuery(peer_query); | |
console.log(stringify(query)) | |
console.log('%s - adding channel peers query', method); | |
} | |
console.log('after reqruei.config ') | |
// add a chaincode query to get endorsement plans | |
if (request.interests && request.interests.length > 0) { | |
const query = new _discoveryProto.Query(); | |
queries.push(query); | |
query.setChannel(this.getName()); | |
console.log('interest ' + stringify(query)) | |
const interests = []; | |
for (const interest of request.interests) { | |
const proto_interest = this._buildProtoChaincodeInterest(interest); | |
interests.push(proto_interest); | |
} | |
const cc_query = new _discoveryProto.ChaincodeQuery(); | |
cc_query.setInterests(interests); | |
query.setCcQuery(cc_query); | |
console.log('%s - adding chaincodes/collection query', method); | |
} | |
// be sure to set the array after completely building it | |
console.log('discover request reqruei.config ' + stringify(queries)) | |
discovery_request.setQueries(queries); | |
// build up the outbound request object | |
const signed_request = client_utils.toEnvelope(client_utils.signProposal(signer, discovery_request)); | |
console.log('target peer discover - before') | |
const response = await target_peer.sendDiscovery(signed_request); | |
console.log('target peer discover - after') | |
console.log('%s - processing discovery response', method); | |
if (response && response.results) { | |
let error_msg = null; | |
console.log('response.results ... ') | |
console.log('%s - parse discovery response', method); | |
for (const index in response.results) { | |
const result = response.results[index]; | |
if (!result) { | |
error_msg = 'Discover results are missing'; | |
break; | |
} else if (result.result === 'error') { | |
console.log('Channel:%s received discovery error:%s', self.getName(), result.error.content); | |
error_msg = result.error.content; | |
break; | |
} else { | |
console.log('%s - process results', method); | |
if (result.config_result) { | |
const config = self._processDiscoveryConfigResults(result.config_result); | |
results.msps = config.msps; | |
results.orderers = config.orderers; | |
} | |
if (result.members) { | |
// local query is always first if included | |
if (request.local && index === '0') { | |
results.local_peers = self._processDiscoveryMembershipResults(result.members); | |
} else { | |
results.peers_by_org = self._processDiscoveryMembershipResults(result.members); | |
} | |
} | |
if (result.cc_query_res) { | |
results.endorsement_plans = self._processDiscoveryChaincodeResults(result.cc_query_res); | |
} | |
console.log('%s - completed processing results', method); | |
} | |
} | |
if (error_msg) { | |
throw Error('Channel:' + self.getName() + ' Discovery error:' + error_msg); | |
} else { | |
return results; | |
} | |
} else { | |
if (response instanceof Error) { | |
if (response.connectFailed) { | |
console.log(' Unable to get discovery results from peer %s', target_peer.getUrl()); | |
// close this peer down so that next time a new connection will be used | |
target_peer.close(); | |
} | |
} | |
throw new Error('Discovery has failed to return results'); | |
} | |
} | |
_processDiscoveryChaincodeResults(q_chaincodes) { | |
const method = '_processDiscoveryChaincodeResults'; | |
console.log('%s - start', method); | |
const endorsement_plans = []; | |
let index; | |
if (q_chaincodes && q_chaincodes.content) { | |
if (Array.isArray(q_chaincodes.content)) { | |
for (index in q_chaincodes.content) { | |
const q_endors_desc = q_chaincodes.content[index]; | |
const endorsement_plan = {}; | |
endorsement_plan.chaincode = q_endors_desc.chaincode; | |
endorsement_plans.push(endorsement_plan); | |
// GROUPS | |
endorsement_plan.groups = {}; | |
for (const group_name in q_endors_desc.endorsers_by_groups) { | |
console.log('%s - found group: %s', method, group_name); | |
const group = {}; | |
group.peers = this._processPeers(q_endors_desc.endorsers_by_groups[group_name].peers); | |
// all done with this group | |
endorsement_plan.groups[group_name] = group; | |
} | |
// LAYOUTS | |
endorsement_plan.layouts = []; | |
for (index in q_endors_desc.layouts) { | |
const q_layout = q_endors_desc.layouts[index]; | |
const layout = {}; | |
for (const group_name in q_layout.quantities_by_group) { | |
layout[group_name] = q_layout.quantities_by_group[group_name]; | |
} | |
console.log('%s - layout :%j', method, layout); | |
endorsement_plan.layouts.push(layout); | |
} | |
} | |
} | |
} | |
return endorsement_plans; | |
} | |
_processDiscoveryConfigResults(q_config) { | |
const method = '_processDiscoveryConfigResults'; | |
console.log('%s - start', method); | |
const config = {}; | |
if (q_config) { | |
try { | |
if (q_config.msps) { | |
config.msps = {}; | |
for (const id in q_config.msps) { | |
console.log('%s - found organization %s', method, id); | |
const q_msp = q_config.msps[id]; | |
const msp_config = { | |
id: id, | |
orgs: q_msp.organizational_unit_identifiers, | |
rootCerts: sdk_utils.convertBytetoString(q_msp.root_certs), | |
intermediateCerts: sdk_utils.convertBytetoString(q_msp.intermediate_certs), | |
admins: sdk_utils.convertBytetoString(q_msp.admins), | |
tls_root_certs: sdk_utils.convertBytetoString(q_msp.tls_root_certs), | |
tls_intermediate_certs: sdk_utils.convertBytetoString(q_msp.tls_intermediate_certs) | |
}; | |
config.msps[id] = msp_config; | |
} | |
} | |
/* | |
"orderers":{"OrdererMSP":{"endpoint":[{"host":"orderer.example.com","port":7050}]}}} | |
*/ | |
if (q_config.orderers) { | |
config.orderers = {}; | |
for (const mspid in q_config.orderers) { | |
console.log('%s - found orderer org: ', method, mspid); | |
config.orderers[mspid] = {}; | |
config.orderers[mspid].endpoints = []; | |
for (const index in q_config.orderers[mspid].endpoint) { | |
config.orderers[mspid].endpoints.push(q_config.orderers[mspid].endpoint[index]); | |
} | |
} | |
} | |
} catch (err) { | |
console.log('Problem with discovery config: %s', err); | |
} | |
} | |
return config; | |
} | |
_processDiscoveryMembershipResults(q_members) { | |
const method = '_processDiscoveryChannelMembershipResults'; | |
console.log('%s - start', method); | |
const peers_by_org = {}; | |
if (q_members && q_members.peers_by_org) { | |
for (const mspid in q_members.peers_by_org) { | |
console.log('%s - found org:%s', method, mspid); | |
peers_by_org[mspid] = {}; | |
peers_by_org[mspid].peers = this._processPeers(q_members.peers_by_org[mspid].peers); | |
} | |
} | |
return peers_by_org; | |
} | |
_processPeers(q_peers) { | |
const method = '_processPeers'; | |
const peers = []; | |
q_peers.forEach((q_peer) => { | |
const peer = {}; | |
// IDENTITY | |
const q_identity = _identityProto.SerializedIdentity.decode(q_peer.identity); | |
peer.mspid = q_identity.mspid; | |
// MEMBERSHIP | |
const q_membership_message = _gossipProto.GossipMessage.decode(q_peer.membership_info.payload); | |
peer.endpoint = q_membership_message.alive_msg.membership.endpoint; | |
console.log('%s - found peer :%s', method, peer.endpoint); | |
// STATE | |
if (q_peer.state_info) { | |
const message_s = _gossipProto.GossipMessage.decode(q_peer.state_info.payload); | |
if (message_s && message_s.state_info && message_s.state_info.properties && message_s.state_info.properties.ledger_height) { | |
peer.ledger_height = Long.fromValue(message_s.state_info.properties.ledger_height); | |
} else { | |
console.log('%s - did not find ledger_height', method); | |
peer.ledger_height = Long.fromValue(0); | |
} | |
console.log('%s - found ledger_height :%s', method, peer.ledger_height); | |
peer.chaincodes = []; | |
for (const index in message_s.state_info.properties.chaincodes) { | |
const q_chaincode = message_s.state_info.properties.chaincodes[index]; | |
const chaincode = {}; | |
chaincode.name = q_chaincode.getName(); | |
chaincode.version = q_chaincode.getVersion(); | |
// TODO metadata ? | |
console.log('%s - found chaincode :%j', method, chaincode); | |
peer.chaincodes.push(chaincode); | |
} | |
} | |
// all done with this peer | |
peers.push(peer); | |
}); | |
return peers; | |
} | |
_buildOrdererName(msp_id, host, port, msps, request) { | |
const method = '_buildOrdererName'; | |
console.log('%s - start', method); | |
const name = host + ':' + port; | |
const url = this._buildUrl(host, port, request); | |
let found = null; | |
this._orderers.forEach((orderer) => { | |
if (orderer.getUrl() === url) { | |
console.log('%s - found existing orderer %s', method, url); | |
found = orderer; | |
} | |
}); | |
if (!found) { | |
if (msps[msp_id]) { | |
console.log('%s - create a new orderer %s', method, url); | |
found = new Orderer(url, this._buildOptions(name, url, host, msps[msp_id])); | |
this.addOrderer(found, true); | |
} else { | |
throw new Error('No TLS cert information available'); | |
} | |
} | |
return found.getName(); | |
} | |
_buildPeerName(endpoint, msp_id, msps, request) { | |
const method = '_buildPeerName'; | |
console.log('%s - start', method); | |
const name = endpoint; | |
const host_port = endpoint.split(':'); | |
const url = this._buildUrl(host_port[0], host_port[1], request); | |
let found = null; | |
this._channel_peers.forEach((peer) => { | |
if (peer.getUrl() === url) { | |
console.log('%s - found existing peer %s', method, url); | |
found = peer; | |
} | |
}); | |
if (!found) { | |
if (msp_id && msps && msps[msp_id]) { | |
console.log('%s - create a new peer %s', method, url); | |
found = new Peer(url, this._buildOptions(name, url, host_port[0], msps[msp_id])); | |
this.addPeer(found, msp_id, null, true); | |
} else { | |
throw new Error('No TLS cert information available'); | |
} | |
} | |
return found.getName(); | |
} | |
_buildUrl(hostname, port, request) { | |
const method = '_buildUrl'; | |
console.log('%s - start', method); | |
let t_hostname = hostname; | |
// endpoints may be running in containers on the local system | |
if (this._as_localhost) { | |
t_hostname = 'localhost'; | |
} | |
const protocol = sdk_utils.getConfigSetting('discovery-protocol', 'grpcs'); | |
const url = protocol + '://' + t_hostname + ':' + port; | |
return url; | |
} | |
_buildOptions(name, url, host, msp) { | |
const method = '_buildOptions'; | |
console.log('%s - start', method); | |
const caroots = this._buildTlsRootCerts(msp); | |
const opts = { | |
'pem': caroots, | |
'ssl-target-name-override': host, | |
'name': name | |
}; | |
this._clientContext.addTlsClientCertAndKey(opts); | |
return opts; | |
} | |
_buildTlsRootCerts(msp) { | |
let caroots = ''; | |
if (msp.tls_root_certs) { | |
caroots = caroots + msp.tls_root_certs; | |
} | |
if (msp.tls_intermediate_certs) { | |
caroots = caroots + msp.tls_intermediate_certs; | |
} | |
return caroots; | |
} | |
/* internal method | |
* Takes an array of {@link DiscoveryChaincodeCall} that represent the | |
* chaincodes and associated collections to build an interest. | |
* The interest becomes part of the query object needed by the discovery | |
* service to calculate the endorsement plan for an invocation. | |
*/ | |
_buildProtoChaincodeInterest(interest) { | |
const chaincode_calls = []; | |
for (const chaincode of interest.chaincodes) { | |
const chaincode_call = new _discoveryProto.ChaincodeCall(); | |
if (typeof chaincode.name === 'string') { | |
chaincode_call.setName(chaincode.name); | |
if (chaincode.collection_names) { | |
if (Array.isArray(chaincode.collection_names)) { | |
const collection_names = []; | |
chaincode.collection_names.map(name => { | |
if (typeof name === 'string') { | |
collection_names.push(name); | |
} else { | |
throw Error('The collection name must be a string'); | |
} | |
}); | |
chaincode_call.setCollectionNames(collection_names); | |
} else { | |
throw Error('collection_names must be an array of strings'); | |
} | |
} | |
chaincode_calls.push(chaincode_call); | |
} else { | |
throw Error('Chaincode name must be a string'); | |
} | |
} | |
const interest_proto = new _discoveryProto.ChaincodeInterest(); | |
interest_proto.setChaincodes(chaincode_calls); | |
return interest_proto; | |
} | |
/* internal method | |
* takes an array of interest and checks to see if they exist on the current | |
* interests, if not adds in any that do not already exist | |
*/ | |
_merge_hints(endorsement_hints) { | |
const method = '_merge_hints'; | |
if (!endorsement_hints) { | |
console.log('%s - no hint return false', method); | |
return false; | |
} | |
let results = false; | |
let hints = endorsement_hints; | |
if (!Array.isArray(endorsement_hints)) { | |
hints = [endorsement_hints]; | |
} | |
for (const hint of hints) { | |
const key = JSON.stringify(hint); | |
const value = this._discovery_interests.get(key); | |
console.log('%s - key %s', method, key); | |
if (value) { | |
console.log('%s - found interest exist %s', method, key); | |
} else { | |
console.log('%s - add new interest %s', method, key); | |
this._discovery_interests.set(key, hint); | |
results = true; | |
} | |
} | |
return results; | |
} | |
/* internal method | |
* takes a single string that represents a chaincode and optional array of strings | |
* that represent collections and builds a JSON | |
* object that may be used as input to building of the GRPC objects to send | |
* to the discovery service. | |
*/ | |
_buildDiscoveryInterest(name, collections) { | |
console.log('_buildDiscoveryInterest - name %s', name); | |
const interest = {}; | |
interest.chaincodes = []; | |
const chaincodes = this._buildDiscoveryChaincodeCall(name, collections); | |
interest.chaincodes.push(chaincodes); | |
return interest; | |
} | |
/* internal metnod | |
* takes a single string name and an array of collection names and builds | |
* a JSON object that may be used as input to the building of the GRPC | |
* objects to send to the discovery service. | |
*/ | |
_buildDiscoveryChaincodeCall(name, collection_names) { | |
const chaincode_call = {}; | |
if (typeof name === 'string') { | |
chaincode_call.name = name; | |
if (collection_names) { | |
if (Array.isArray(collection_names)) { | |
chaincode_call.collection_names = []; | |
collection_names.map(name1 => { | |
if (typeof name1 === 'string') { | |
chaincode_call.collection_names.push(name1); | |
} else { | |
throw Error('The collection name must be a string'); | |
} | |
}); | |
} else { | |
throw Error('Collections names must be an array of strings'); | |
} | |
} | |
} else { | |
throw Error('Chaincode name must be a string'); | |
} | |
return chaincode_call; | |
} | |
/** | |
* A protobuf message that gets returned by endorsing peers on proposal requests. | |
* The peer node runs the target chaincode, as designated by the proposal, and | |
* decides on whether to endorse the proposal or not, and sends back the endorsement | |
* result along with the [read and write sets]{@link http://hyperledger-fabric.readthedocs.io/en/latest/arch-deep-dive.html?highlight=readset#the-endorsing-peer-simulates-a-transaction-and-produces-an-endorsement-signature} | |
* inside the proposal response message. | |
* | |
* @typedef {Object} ProposalResponse | |
* @property {number} version | |
* @property {Timestamp} timestamp - Time the proposal was created by the submitter | |
* @property {Response} response | |
* @property {byte[]} payload - The payload of the response. It is the encoded | |
* bytes of the "ProposalResponsePayload" protobuf message | |
* @property {Endorsement} endorsement - The endorsement of the proposal, | |
* basically the endorser's signature over the payload | |
* @property {RemoteCharacteristics} peer - The characteristics of the peer | |
* that created this ProposalResponse. Items include the url, name, | |
* and connection options. | |
* This information is not returned from the peer, it is not part of | |
* the serialized protobuf data returned by the peer. This information | |
* is added by the client instance peer object to help identify | |
* this ProposalResponse object. | |
*/ | |
/** | |
* Information related to the peer instance object. | |
* | |
* @typedef {Object} RemoteCharacteristics | |
* @property {string} url - The url of this peer | |
* @property {string} name - The name of this peer, this will be the host and port | |
* if the peer was not created with the name option. | |
* @property {Object} options - The options object that this peer built | |
* based on defaults and what was passed when it was created. | |
* Typical options will include the GRPC connection settings. | |
*/ | |
/** | |
* A response message indicating whether the endorsement of the proposal was successful | |
* | |
* @typedef {Object} Response | |
* @property {number} status - Status code. Follows [HTTP status code definitions]{@link https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} | |
* @property {string} message - A message associated with the response status code | |
* @property {byte[]} payload - A payload that can be used to include metadata with this response | |
*/ | |
/** | |
* @typedef {Object} JoinChannelRequest | |
* @property {Peer[] | string[]} targets - Optional. An array of Peer objects or Peer names that will | |
* be asked to join this channel. When using Peer names or left | |
* empty (use default targets) there must be a loaded network | |
* configuration. | |
* See [loadFromConfig()]{@link Client#loadFromConfig} | |
* @property {byte[]} block - The encoded bytes of the channel's genesis block. | |
* See [getGenesisBlock()]{@link Channel#getGenesisBlock} method | |
* @property {TransactionID} txId - Required. TransactionID object with the transaction id and nonce | |
*/ | |
/** | |
* For a peer node to become part of a channel, it must be sent the genesis | |
* block, as explained [here]{@link Channel#getGenesisBlock}. This method | |
* sends a join channel proposal to one or more endorsing peers. | |
* | |
* @param {JoinChannelRequest} request | |
* @param {Number} timeout - A number indicating milliseconds to wait on the | |
* response before rejecting the promise with a | |
* timeout error. This overrides the default timeout | |
* of the {@link Peer} instance(s) and the global timeout in the config settings. | |
* @returns {Promise} A Promise for an array of {@link ProposalResponse} from the target peers | |
*/ | |
joinChannel(request, timeout) { | |
console.log('joinChannel - start'); | |
let errorMsg = null; | |
// verify that we have targets (Peers) to join this channel | |
// defined by the caller | |
if (!request) { | |
errorMsg = 'Missing all required input request parameters'; | |
} else if (!request.txId) { // verify that we have transaction id | |
errorMsg = 'Missing txId input parameter with the required transaction identifier'; | |
} else if (!request.block) { | |
errorMsg = 'Missing block input parameter with the required genesis block'; | |
} | |
if (errorMsg) { | |
console.log('joinChannel - error ' + errorMsg); | |
throw new Error(errorMsg); | |
} | |
const targets = this._getTargets(request.targets, 'ALL ROLES'); | |
const signer = this._clientContext._getSigningIdentity(request.txId.isAdmin()); | |
const chaincodeInput = new _ccProto.ChaincodeInput(); | |
const args = []; | |
args.push(Buffer.from('JoinChain', 'utf8')); | |
args.push(request.block.toBuffer()); | |
chaincodeInput.setArgs(args); | |
const chaincodeID = new _ccProto.ChaincodeID(); | |
chaincodeID.setName(Constants.CSCC); | |
const chaincodeSpec = new _ccProto.ChaincodeSpec(); | |
chaincodeSpec.setType(_ccProto.ChaincodeSpec.Type.GOLANG); | |
chaincodeSpec.setChaincodeId(chaincodeID); | |
chaincodeSpec.setInput(chaincodeInput); | |
const channelHeader = client_utils.buildChannelHeader( | |
_commonProto.HeaderType.ENDORSER_TRANSACTION, | |
'', | |
request.txId.getTransactionID(), | |
null, // no epoch | |
Constants.CSCC, | |
client_utils.buildCurrentTimestamp(), | |
this._clientContext.getClientCertHash() | |
); | |
const header = client_utils.buildHeader(signer, channelHeader, request.txId.getNonce()); | |
const proposal = client_utils.buildProposal(chaincodeSpec, header); | |
const signed_proposal = client_utils.signProposal(signer, proposal); | |
return client_utils.sendPeersProposal(targets, signed_proposal, timeout).catch((err) => { | |
console.log('joinChannel - Failed Proposal. Error: %s', err.stack ? err.stack : err); | |
return Promise.reject(err); | |
}); | |
} | |
/** | |
* Asks the peer for the current (latest) configuration block for this channel. | |
* @param {string | Peer} target - Optional. The peer to be used to make the | |
* request. | |
* @param {Number} timeout - Optional. A number indicating milliseconds to wait | |
* on the response before rejecting the promise with a | |
* timeout error. This overrides the default timeout | |
* of the {@link Peer} instance(s) and the global timeout | |
* in the config settings. | |
* @returns {Promise} A Promise for a {@link ConfigEnvelope} object containing the configuration items. | |
*/ | |
async getChannelConfig(target, timeout) { | |
const method = 'getChannelConfig'; | |
console.log('%s - start for channel %s', method, this._name); | |
const targets = this._getTargetForQuery(target); | |
const signer = this._clientContext._getSigningIdentity(true); | |
const tx_id = new TransactionID(signer, true); | |
const request = { | |
targets: targets, | |
chaincodeId: Constants.CSCC, | |
txId: tx_id, | |
signer: signer, | |
fcn: 'GetConfigBlock', | |
args: [this._name] | |
}; | |
request.targets = this._getTargets(request.targets, Constants.NetworkConfig.ENDORSING_PEER_ROLE); | |
const results = await Channel.sendTransactionProposal(request, this._name, this._clientContext, timeout); | |
const responses = results[0]; | |
// const proposal = results[1]; | |
console.log('%s - results received', method); | |
if (responses && Array.isArray(responses)) { | |
const response = responses[0]; | |
if (response instanceof Error) { | |
throw response; | |
} else if (response.response && response.response.payload && response.response.status === 200) { | |
const block = _commonProto.Block.decode(response.response.payload); | |
const envelope = _commonProto.Envelope.decode(block.data.data[0]); | |
const payload = _commonProto.Payload.decode(envelope.payload); | |
const config_envelope = _configtxProto.ConfigEnvelope.decode(payload.data); | |
return config_envelope; | |
} else { | |
console.log('%s - unknown response ::%s', method, response); | |
throw new Error(response); | |
} | |
} | |
throw new Error('Payload results are missing from the get channel config'); | |
} | |
/** | |
* Asks the orderer for the current (latest) configuration block for this channel. | |
* This is similar to [getGenesisBlock()]{@link Channel#getGenesisBlock}, except | |
* that instead of getting block number 0 it gets the latest block that contains | |
* the channel configuration, and only returns the decoded {@link ConfigEnvelope}. | |
* | |
* @returns {Promise} A Promise for a {@link ConfigEnvelope} object containing the configuration items. | |
*/ | |
async getChannelConfigFromOrderer() { | |
const method = 'getChannelConfigFromOrderer'; | |
console.log('%s - start for channel %s', method, this._name); | |
const self = this; | |
const orderer = this._clientContext.getTargetOrderer(null, this.getOrderers(), this._name); | |
const signer = this._clientContext._getSigningIdentity(true); | |
let txId = new TransactionID(signer, true); | |
// seek the latest block | |
let seekSpecifiedStart = new _abProto.SeekNewest(); | |
let seekStart = new _abProto.SeekPosition(); | |
seekStart.setNewest(seekSpecifiedStart); | |
let seekSpecifiedStop = new _abProto.SeekNewest(); | |
let seekStop = new _abProto.SeekPosition(); | |
seekStop.setNewest(seekSpecifiedStop); | |
// seek info with all parts | |
let seekInfo = new _abProto.SeekInfo(); | |
seekInfo.setStart(seekStart); | |
seekInfo.setStop(seekStop); | |
seekInfo.setBehavior(_abProto.SeekInfo.SeekBehavior.BLOCK_UNTIL_READY); | |
// build the header for use with the seekInfo payload | |
let seekInfoHeader = client_utils.buildChannelHeader( | |
_commonProto.HeaderType.DELIVER_SEEK_INFO, | |
self._name, | |
txId.getTransactionID(), | |
self._initial_epoch, | |
null, | |
client_utils.buildCurrentTimestamp(), | |
this._clientContext.getClientCertHash() | |
); | |
let seekHeader = client_utils.buildHeader(signer, seekInfoHeader, txId.getNonce()); | |
let seekPayload = new _commonProto.Payload(); | |
seekPayload.setHeader(seekHeader); | |
seekPayload.setData(seekInfo.toBuffer()); | |
// building manually or will get protobuf errors on send | |
let envelope = client_utils.toEnvelope(client_utils.signProposal(signer, seekPayload)); | |
// This will return us a block | |
let block = await orderer.sendDeliver(envelope); | |
console.log('%s - good results from seek block ', method); // :: %j',results); | |
// verify that we have the genesis block | |
if (block) { | |
console.log('%s - found latest block', method); | |
} else { | |
console.log('%s - did not find latest block', method); | |
throw new Error('Failed to retrieve latest block', method); | |
} | |
console.log('%s - latest block is block number %s', block.header.number); | |
// get the last config block number | |
const metadata = _commonProto.Metadata.decode(block.metadata.metadata[_commonProto.BlockMetadataIndex.LAST_CONFIG]); | |
const last_config = _commonProto.LastConfig.decode(metadata.value); | |
console.log('%s - latest block has config block of %s', method, last_config.index); | |
txId = new TransactionID(signer); | |
// now build the seek info to get the block called out | |
// as the latest config block | |
seekSpecifiedStart = new _abProto.SeekSpecified(); | |
seekSpecifiedStart.setNumber(last_config.index); | |
seekStart = new _abProto.SeekPosition(); | |
seekStart.setSpecified(seekSpecifiedStart); | |
// build stop | |
seekSpecifiedStop = new _abProto.SeekSpecified(); | |
seekSpecifiedStop.setNumber(last_config.index); | |
seekStop = new _abProto.SeekPosition(); | |
seekStop.setSpecified(seekSpecifiedStop); | |
// seek info with all parts | |
seekInfo = new _abProto.SeekInfo(); | |
seekInfo.setStart(seekStart); | |
seekInfo.setStop(seekStop); | |
seekInfo.setBehavior(_abProto.SeekInfo.SeekBehavior.BLOCK_UNTIL_READY); | |
// console.log('initializeChannel - seekInfo ::' + JSON.stringify(seekInfo)); | |
// build the header for use with the seekInfo payload | |
seekInfoHeader = client_utils.buildChannelHeader( | |
_commonProto.HeaderType.DELIVER_SEEK_INFO, | |
self._name, | |
txId.getTransactionID(), | |
self._initial_epoch, | |
null, | |
client_utils.buildCurrentTimestamp(), | |
self._clientContext.getClientCertHash() | |
); | |
seekHeader = client_utils.buildHeader(signer, seekInfoHeader, txId.getNonce()); | |
seekPayload = new _commonProto.Payload(); | |
seekPayload.setHeader(seekHeader); | |
seekPayload.setData(seekInfo.toBuffer()); | |
// building manually or will get protobuf errors on send | |
envelope = client_utils.toEnvelope(client_utils.signProposal(signer, seekPayload)); | |
// this will return us a block | |
block = await orderer.sendDeliver(envelope); | |
if (!block) { | |
throw new Error('Config block was not found'); | |
} | |
// lets have a look at the block | |
console.log('%s - config block number ::%s -- numberof tx :: %s', method, block.header.number, block.data.data.length); | |
if (block.data.data.length !== 1) { | |
throw new Error('Config block must only contain one transaction'); | |
} | |
envelope = _commonProto.Envelope.decode(block.data.data[0]); | |
const payload = _commonProto.Payload.decode(envelope.payload); | |
const channel_header = _commonProto.ChannelHeader.decode(payload.header.channel_header); | |
if (channel_header.type !== _commonProto.HeaderType.CONFIG) { | |
throw new Error(`Block must be of type "CONFIG" (${_commonProto.HeaderType.CONFIG}), but got "${channel_header.type}" instead`); | |
} | |
const config_envelope = _configtxProto.ConfigEnvelope.decode(payload.data); | |
// send back the envelope | |
return config_envelope; | |
} | |
loadConfigUpdate(config_update_bytes) { | |
const config_update = _configtxProto.ConfigUpdate.decode(config_update_bytes); | |
console.log('loadConfigData - channel ::' + config_update.channel_id); | |
const read_group = config_update.read_set; | |
const write_group = config_update.write_set; | |
const config_items = {}; | |
config_items.msps = []; // save all the MSP's found | |
config_items['anchor-peers'] = []; // save all the MSP's found | |
config_items.orderers = []; | |
config_items['kafka-brokers'] = []; | |
config_items.settings = {}; | |
config_items.versions = {}; | |
config_items.versions.read_group = {}; | |
config_items.versions.write_group = {}; | |
loadConfigGroup(config_items, config_items.versions.read_group, read_group, 'read_set', null, true); | |
// do the write_set second so they update anything in the read set | |
loadConfigGroup(config_items, config_items.versions.write_group, write_group, 'write_set', null, true); | |
this._msp_manager.loadMSPs(config_items.msps); | |
this._anchor_peers = config_items.anchor_peers; | |
// TODO should we create orderers and endorsing peers | |
return config_items; | |
} | |
/* | |
* Utility method to load this channel with configuration information | |
* from a Configuration block | |
* @param {ConfigEnvelope} the envelope with the configuration items | |
* @see /protos/common/configtx.proto | |
*/ | |
loadConfigEnvelope(config_envelope) { | |
console.log('loadConfigEnvelope - start'); | |
const group = config_envelope.config.channel_group; | |
const config_items = {}; | |
config_items.msps = []; // save all the MSP's found | |
config_items['anchor-peers'] = []; // save all the MSP's found | |
config_items.orderers = []; | |
config_items['kafka-brokers'] = []; | |
config_items.versions = {}; | |
config_items.versions.channel = {}; | |
loadConfigGroup(config_items, config_items.versions.channel, group, 'base', null, true); | |
this._msp_manager.loadMSPs(config_items.msps); | |
this._anchor_peers = config_items.anchor_peers; | |
// TODO should we create orderers and endorsing peers | |
return config_items; | |
} | |
/** | |
* @typedef {Object} BlockchainInfo | |
* @property {number} height - How many blocks exist on the channel's ledger | |
* @property {byte[]} currentBlockHash - A block hash is calculated by hashing over the concatenated | |
* ASN.1 encoded bytes of: the block number, previous block hash, | |
* and current block data hash. It's the chain of the block | |
* hashs that guarantees the immutability of the ledger | |
* @property {byte[]} previousBlockHash - The block hash of the previous block. | |
*/ | |
/** | |
* Queries for various useful information on the state of the Channel | |
* (height, known peers). | |
* | |
* @param {Peer} target - Optional. The peer that is the target for this query. If no target is passed, | |
* the query will use the first peer that was added to the channel object. | |
* @param {boolean} useAdmin - Optional. Indicates that the admin credentials should be used in making | |
* this call to the peer. | |
* @returns {Promise} A Promise for a {@link BlockchainInfo} object with blockchain height, | |
* current block hash and previous block hash. | |
*/ | |
async queryInfo(target, useAdmin) { | |
console.log('queryInfo - start'); | |
const targets = this._getTargetForQuery(target); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const tx_id = new TransactionID(signer, useAdmin); | |
const request = { | |
targets: targets, | |
chaincodeId: Constants.QSCC, | |
txId: tx_id, | |
signer: signer, | |
fcn: 'GetChainInfo', | |
args: [this._name] | |
}; | |
const results = await Channel.sendTransactionProposal(request, this._name, this._clientContext, null); | |
const responses = results[0]; | |
if (responses && Array.isArray(responses)) { | |
console.log('queryInfo - got responses=' + responses.length); | |
// will only be one response as we are only querying the primary peer | |
if (responses.length > 1) { | |
throw new Error('Too many results returned'); | |
} | |
const response = responses[0]; | |
if (response instanceof Error) { | |
throw response; | |
} | |
if (response.response && response.response.status && response.response.status === 200) { | |
console.log('queryInfo - response status %d:', response.response.status); | |
return _ledgerProto.BlockchainInfo.decode(response.response.payload); | |
} else if (response.response && response.response.status) { | |
// no idea what we have, lets fail it and send it back | |
throw new Error(response.response.message); | |
} | |
} | |
throw new Error('Payload results are missing from the query channel info'); | |
} | |
/** | |
* Queries the ledger on the target peer for a Block TransactionID. | |
* | |
* @param {string} tx_id - The TransactionID of the Block in question. | |
* @param {Peer} target - Optional. The peer to send the query to. If no target is passed, | |
* the query is sent to the first peer that was added to the channel object. | |
* @param {boolean} useAdmin - Optional. Indicates that the admin credentials should be used in making | |
* this call to the peer. | |
* @param {boolean} skipDecode - Optional. If true, this function returns an encoded block. | |
* @returns {Promise} A Promise for a {@link Block} matching the tx_id, fully decoded into an object. | |
*/ | |
async queryBlockByTxID(tx_id, target, useAdmin, skipDecode) { | |
console.log('queryBlockByTxID - start'); | |
if (!tx_id || !(typeof tx_id === 'string')) { | |
throw new Error('tx_id as string is required'); | |
} | |
const args = [this._name, tx_id]; | |
const targets = this._getTargetForQuery(target); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const request = { | |
targets, | |
chaincodeId: Constants.QSCC, | |
txId: new TransactionID(signer, useAdmin), | |
fcn: 'GetBlockByTxID', | |
args | |
}; | |
const results = await Channel.sendTransactionProposal(request, this._name, this._clientContext, null); | |
const responses = results[0]; | |
if (responses && Array.isArray(responses)) { | |
console.log('queryBlockByTxID - got response', responses.length); | |
// will only be one response as we are only querying the primary peer | |
if (responses.length > 1) { | |
throw new Error('Too many results returned'); | |
} | |
const response = responses[0]; | |
if (response instanceof Error) { | |
throw response; | |
} | |
if (response.response && response.response.status && response.response.status === 200) { | |
console.log('queryBlockByTxID - response status %d:', response.response.status); | |
if (skipDecode) { | |
return response.response.payload; | |
} else { | |
const block = BlockDecoder.decode(response.response.payload); | |
console.log('queryBlockByTxID - looking at block :: %s', block.header.number); | |
return block; | |
} | |
} else if (response.response && response.response.status) { | |
// no idea what we have, lets fail it and send it back | |
throw new Error(response.response.message); | |
} | |
} | |
throw new Error('Payload results are missing from the query'); | |
} | |
/** | |
* Queries the ledger on the target peer for a Block by block hash. | |
* | |
* @param {byte[]} blockHash of the Block in question. | |
* @param {Peer} target - Optional. The peer to send the query to. If no target is passed, | |
* the query is sent to the first peer that was added to the channel object. | |
* @param {boolean} useAdmin - Optional. Indicates that the admin credentials should be used in making | |
* this call to the peer. | |
* @param {boolean} skipDecode - Optional. If true, this function returns an encoded block. | |
* @returns {Promise} A Promise for a {@link Block} matching the hash, fully decoded into an object. | |
*/ | |
async queryBlockByHash(blockHash, target, useAdmin, skipDecode) { | |
console.log('queryBlockByHash - start'); | |
if (!blockHash) { | |
throw new Error('Blockhash bytes are required'); | |
} | |
const targets = this._getTargetForQuery(target); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const txId = new TransactionID(signer, useAdmin); | |
const request = { | |
targets: targets, | |
chaincodeId: Constants.QSCC, | |
txId: txId, | |
signer: signer, | |
fcn: 'GetBlockByHash', | |
args: [this._name], | |
argbytes: blockHash | |
}; | |
const results = await Channel.sendTransactionProposal(request, this._name, this._clientContext, null); | |
const responses = results[0]; | |
console.log('queryBlockByHash - got response'); | |
if (responses && Array.isArray(responses)) { | |
// will only be one response as we are only querying the primary peer | |
if (responses.length > 1) { | |
throw new Error('Too many results returned'); | |
} | |
const response = responses[0]; | |
if (response instanceof Error) { | |
throw response; | |
} | |
if (response.response && response.response.status && response.response.status === 200) { | |
console.log('queryBlockByHash - response status %d:', response.response.status); | |
if (skipDecode) { | |
return response.response.payload; | |
} else { | |
const block = BlockDecoder.decode(response.response.payload); | |
console.log('queryBlockByHash - looking at block :: %s', block.header.number); | |
return block; | |
} | |
} else if (response.response && response.response.status) { | |
// no idea what we have, lets fail it and send it back | |
throw new Error(response.response.message); | |
} | |
} | |
throw new Error('Payload results are missing from the query'); | |
} | |
/** | |
* Queries the ledger on the target peer for Block by block number. | |
* | |
* @param {number} blockNumber - The number of the Block in question. | |
* @param {Peer} target - Optional. The peer to send this query to. If no target is passed, | |
* the query is sent to the first peer that was added to the channel object. | |
* @param {boolean} useAdmin - Optional. Indicates that the admin credentials should be used in making | |
* this call to the peer. | |
* @param {boolean} skipDecode - Optional. If true, this function returns an encoded block. | |
* @returns {Promise} A Promise for a {@link Block} at the blockNumber slot in the ledger, fully decoded into an object. | |
*/ | |
async queryBlock(blockNumber, target, useAdmin, skipDecode) { | |
console.log('queryBlock - start blockNumber %s', blockNumber); | |
let block_number = null; | |
if (Number.isInteger(blockNumber) && blockNumber >= 0) { | |
block_number = blockNumber.toString(); | |
} else { | |
throw new Error('Block number must be a positive integer'); | |
} | |
const targets = this._getTargetForQuery(target); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const txId = new TransactionID(signer, useAdmin); | |
const request = { | |
targets: targets, | |
chaincodeId: Constants.QSCC, | |
txId: txId, | |
signer: signer, | |
fcn: 'GetBlockByNumber', | |
args: [this._name, block_number] | |
}; | |
const results = await Channel.sendTransactionProposal(request, this._name, this._clientContext, null); | |
const responses = results[0]; | |
console.log('queryBlock - got response'); | |
if (responses && Array.isArray(responses)) { | |
// will only be one response as we are only querying the primary peer | |
if (responses.length > 1) { | |
throw new Error('Too many results returned'); | |
} | |
const response = responses[0]; | |
if (response instanceof Error) { | |
throw response; | |
} | |
if (response.response && response.response.status && response.response.status === 200) { | |
console.log('queryBlock - response status %d:', response.response.status); | |
if (skipDecode) { | |
return response.response.payload; | |
} else { | |
const block = BlockDecoder.decode(response.response.payload); | |
console.log('queryBlock - looking at block :: %s', block.header.number); | |
return block; | |
} | |
} else if (response.response && response.response.status) { | |
// no idea what we have, lets fail it and send it back | |
throw new Error(response.response.message); | |
} | |
} | |
throw new Error('Payload results are missing from the query'); | |
} | |
/** | |
* Queries the ledger on the target peer for Transaction by id. | |
* | |
* @param {string} tx_id - The id of the transaction | |
* @param {Peer} target - Optional. The peer to send this query to. If no target is passed, | |
* the query is sent to the first peer that was added to the channel object. | |
* @param {boolean} useAdmin - Optional. Indicates that the admin credentials should be used in making | |
* this call to the peer. | |
* @param {boolean} skipDecode - Optional. If true, this function returns an encoded transaction. | |
* @returns {Promise} A Promise for a fully decoded {@link ProcessedTransaction} object. | |
*/ | |
async queryTransaction(tx_id, target, useAdmin, skipDecode) { | |
console.log('queryTransaction - start transactionID %s', tx_id); | |
if (tx_id) { | |
tx_id = tx_id.toString(); | |
} else { | |
throw new Error('Missing "tx_id" parameter'); | |
} | |
const targets = this._getTargetForQuery(target); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const txId = new TransactionID(signer, useAdmin); | |
const request = { | |
targets: targets, | |
chaincodeId: Constants.QSCC, | |
txId: txId, | |
signer: signer, | |
fcn: 'GetTransactionByID', | |
args: [this._name, tx_id] | |
}; | |
const results = await Channel.sendTransactionProposal(request, this._name, this._clientContext, null); | |
const responses = results[0]; | |
console.log('queryTransaction - got response'); | |
if (responses && Array.isArray(responses)) { | |
// will only be one response as we are only querying the primary peer | |
if (responses.length > 1) { | |
throw new Error('Too many results returned'); | |
} | |
const response = responses[0]; | |
if (response instanceof Error) { | |
throw response; | |
} | |
if (response.response && response.response.status && response.response.status === 200) { | |
console.log('queryTransaction - response status :: %d', response.response.status); | |
if (skipDecode) { | |
return response.response.payload; | |
} else { | |
return BlockDecoder.decodeTransaction(response.response.payload); | |
} | |
} else if (response.response && response.response.status) { | |
// no idea what we have, lets fail it and send it back | |
throw new Error(response.response.message); | |
} | |
} | |
throw new Error('Payload results are missing from the query'); | |
} | |
/** | |
* Queries the ledger on the target peer for instantiated chaincodes on this channel. | |
* | |
* @param {Peer} target - Optional. The peer to send this query to. If no | |
* target is passed, the query is sent to the first peer that was | |
* added to the channel object. | |
* @param {boolean} useAdmin - Optional. Indicates that the admin credentials | |
* should be used in making this call to the peer. An administrative | |
* identity must have been loaded by common connection profile or by | |
* using the 'setAdminSigningIdentity' method. | |
* @returns {Promise} A Promise for a fully decoded {@link ChaincodeQueryResponse} object. | |
*/ | |
async queryInstantiatedChaincodes(target, useAdmin) { | |
console.log('queryInstantiatedChaincodes - start'); | |
const targets = this._getTargetForQuery(target); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const txId = new TransactionID(signer, useAdmin); | |
const request = { | |
targets: targets, | |
chaincodeId: Constants.LSCC, | |
txId: txId, | |
signer: signer, | |
fcn: 'getchaincodes', | |
args: [] | |
}; | |
const results = await Channel.sendTransactionProposal(request, this._name, this._clientContext, null); | |
const responses = results[0]; | |
console.log('queryInstantiatedChaincodes - got response'); | |
if (responses && Array.isArray(responses)) { | |
// will only be one response as we are only querying one peer | |
if (responses.length > 1) { | |
throw new Error('Too many results returned'); | |
} | |
const response = responses[0]; | |
if (response instanceof Error) { | |
throw response; | |
} | |
if (response.response) { | |
if (response.response.status === 200) { | |
console.log('queryInstantiatedChaincodes - response status :: %d', response.response.status); | |
const queryTrans = _queryProto.ChaincodeQueryResponse.decode(response.response.payload); | |
console.log('queryInstantiatedChaincodes - ProcessedTransaction.chaincodeInfo.length :: %s', queryTrans.chaincodes.length); | |
for (const chaincode of queryTrans.chaincodes) { | |
console.log('queryInstantiatedChaincodes - name %s, version %s, path %s', chaincode.name, chaincode.version, chaincode.path); | |
} | |
return queryTrans; | |
} else { | |
if (response.response.message) { | |
throw new Error(response.response.message); | |
} | |
} | |
} | |
// no idea what we have, lets fail it and send it back | |
throw new Error(response); | |
} | |
throw new Error('Payload results are missing from the query'); | |
} | |
/** | |
* @typedef {Object} CollectionQueryOptions | |
* @property {string} chaincodeId - Required. Name of the chaincode | |
* @property {string|Peer} target - Optional. The peer that will receive this | |
* request, when not provided the first peer in this channel | |
* object will be used. | |
*/ | |
/** | |
* @typedef {Object} CollectionQueryResponse | |
* @property {string} type - The collection type | |
* @property {string} name - the name of the collection inside the denoted chaincode | |
* @property {number} required_peer_count - The minimum number of peers private data | |
* will be sent to upon endorsement. The endorsement would fail if dissemination to | |
* at least this number of peers is not achieved. | |
* @property {number} maximum_peer_count - The maximum number of peers that private | |
* data will be sent to upon endorsement. This number has to be bigger than | |
* required_peer_count. | |
* @property {number} block_to_live - The number of blocks after which the collection | |
* data expires. For instance if the value is set to 10, a key last modified by block | |
* number 100 will be purged at block number 111. A zero value is treated same as MaxUint64, | |
* where the data will not be purged. | |
* @property {Policy} policy - The | |
*/ | |
/** | |
* Query for the collection definitions associated with a chaincode. | |
* | |
* @param {CollectionQueryOptions} options - Required. The options to query the collections config. | |
* @param {boolean} useAdmin - Optional. To indicate that the admin identity | |
* should be used to make the query request | |
* @returns {Promise<CollectionQueryResponse[]>} returns a promise for an array of | |
* {@link CollectionQueryResponse} objects. | |
*/ | |
async queryCollectionsConfig(options, useAdmin) { | |
const method = 'queryCollectionsConfig'; | |
console.log('%s - start. options:%j, useAdmin:%s', method, options, useAdmin); | |
if (!options || !options.chaincodeId || typeof options.chaincodeId !== 'string') { | |
throw new Error('Missing required argument \'options.chaincodeId\' or \'options.chaincodeId\' is not of type string'); | |
} | |
const targets = this._getTargetForQuery(options.target); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const txId = new TransactionID(signer, useAdmin); | |
const request = { | |
targets, | |
txId, | |
signer, | |
chaincodeId: Constants.LSCC, | |
fcn: 'GetCollectionsConfig', | |
args: [options.chaincodeId], | |
}; | |
try { | |
const [responses] = await Channel.sendTransactionProposal(request, this._name, this._clientContext, null); | |
if (responses && Array.isArray(responses)) { | |
if (responses.length > 1) { | |
throw new Error('Too many results returned'); | |
} | |
const [response] = responses; | |
if (response instanceof Error) { | |
throw response; | |
} | |
if (!response.response) { | |
throw new Error('Didn\'t receive a valid peer response'); | |
} | |
console.log('%s - response status :: %d', method, response.response.status); | |
if (response.response.status !== 200) { | |
console.log('%s - response:%j', method, response); | |
if (response.response.message) { | |
throw new Error(response.response.message); | |
} | |
throw new Error('Failed to retrieve collections config from peer'); | |
} | |
const queryResponse = decodeCollectionsConfig(response.response.payload); | |
console.log('%s - get %s collections for chaincode %s from peer', method, queryResponse.length, options.chaincodeId); | |
return queryResponse; | |
} | |
throw new Error('Failed to retrieve collections config from peer'); | |
} catch (e) { | |
throw e; | |
} | |
} | |
/** | |
* @typedef {Object} ChaincodeInstantiateUpgradeRequest | |
* @property {Peer[] | string[]} targets - Optional. An array of endorsing | |
* {@link Peer} objects or peer names as the targets of the request. | |
* When this parameter is omitted the target list will include peers assigned | |
* to this channel instance that are in the endorsing role. | |
* @property {string} chaincodeType - Optional. Type of chaincode. One of | |
* 'golang', 'car', 'java' or 'node'. Default is 'golang'. Note that 'java' | |
* is not yet supported. | |
* @property {string} chaincodeId - Required. The name of the chaincode | |
* @property {string} chaincodeVersion - Required. Version string of the chaincode, | |
* such as 'v1' | |
* @property {TransactionID} txId - Required. Object with the transaction id | |
* and nonce | |
* @property {string} collections-config - Optional. The path to the collections | |
* config. More details can be found at this [tutorial]{@link https://fabric-sdk-node.github.io/tutorial-private-data.html} | |
* @property {object} [transientMap] - Optional. Object with String property names | |
* and Buffer property values that can be used by the chaincode but not | |
* saved in the ledger. Data such as cryptographic information for | |
* encryption can be passed to the chaincode using this technique. Data | |
* that is to be kept in a private data store (a collection) should be | |
* passed to the chaincode in the transientMap. | |
* @property {string} fcn - Optional. The function name to be returned when | |
* calling <code>stub.GetFunctionAndParameters()</code> in the target | |
* chaincode. Default is 'init'. | |
* @property {string[]} args - Optional. Array of string arguments to pass to | |
* the function identified by the <code>fcn</code> value. | |
* @property {Object} endorsement-policy - Optional. EndorsementPolicy object | |
* for this chaincode (see examples below). If not specified, a default | |
* policy of "a signature by any member from any of the organizations | |
* corresponding to the array of member service providers" is used. | |
* <b>WARNING:</b> The default policy is NOT recommended for production, | |
* because this allows an application to bypass the proposal endorsement | |
* and send a manually constructed transaction, with arbitrary output | |
* in the write set, to the orderer directly. The user context assigned | |
* to the client instance that creates the signature will allow the | |
* transaction to be successfully validated | |
* and committed to the ledger. | |
* @example <caption>Endorsement policy: "Signed by any member from one of the organizations"</caption> | |
* { | |
* identities: [ | |
* { role: { name: "member", mspId: "org1" }}, | |
* { role: { name: "member", mspId: "org2" }} | |
* ], | |
* policy: { | |
* "1-of": [{ "signed-by": 0 }, { "signed-by": 1 }] | |
* } | |
* } | |
* @example <caption>Endorsement policy: "Signed by admin of the ordererOrg and any member from one of the peer organizations"</caption> | |
* { | |
* identities: [ | |
* { role: { name: "member", mspId: "peerOrg1" }}, | |
* { role: { name: "member", mspId: "peerOrg2" }}, | |
* { role: { name: "admin", mspId: "ordererOrg" }} | |
* ], | |
* policy: { | |
* "2-of": [ | |
* { "signed-by": 2}, | |
* { "1-of": [{ "signed-by": 0 }, { "signed-by": 1 }]} | |
* ] | |
* } | |
* } | |
*/ | |
/** | |
* Sends a chaincode instantiate proposal to one or more endorsing peers. | |
* | |
* A chaincode must be instantiated on a channel-by-channel basis before it can | |
* be used. The chaincode must first be installed on the endorsing peers where | |
* this chaincode is expected to run, by calling [client.installChaincode()]{@link Client#installChaincode}. | |
* <br><br> | |
* Instantiating a chaincode is a full transaction operation, meaning it must be | |
* first endorsed as a proposal, then the endorsements are sent to the orderer | |
* to be processed for ordering and validation. When the transaction finally gets | |
* committed to the channel's ledger on the peers, the chaincode is then considered | |
* activated and the peers are ready to take requests to process transactions. | |
* | |
* @param {ChaincodeInstantiateUpgradeRequest} request | |
* @param {Number} timeout - A number indicating milliseconds to wait on the | |
* response before rejecting the promise with a | |
* timeout error. This overrides the default timeout | |
* of the Peer instance and the global timeout in the config settings. | |
* @returns {Promise} A Promise for the {@link ProposalResponseObject} | |
*/ | |
sendInstantiateProposal(request, timeout) { | |
return this._sendChaincodeProposal(request, 'deploy', timeout); | |
} | |
/** | |
* Sends a chaincode upgrade proposal to one or more endorsing peers. | |
* | |
* Upgrading a chaincode involves steps similar to instantiating a chaincode. | |
* The new chaincode must first be installed on the endorsing peers where | |
* this chaincode is expected to run. | |
* <br><br> | |
* Similar to instantiating a chaincode, upgrading chaincodes is also a full transaction | |
* operation. | |
* | |
* @param {ChaincodeInstantiateUpgradeRequest} request | |
* @param {Number} timeout - A number indicating milliseconds to wait on the | |
* response before rejecting the promise with a | |
* timeout error. This overrides the default timeout | |
* of the Peer instance and the global timeout in the config settings. | |
* @returns {Promise} A Promise for the {@link ProposalResponseObject} | |
*/ | |
sendUpgradeProposal(request, timeout) { | |
return this._sendChaincodeProposal(request, 'upgrade', timeout); | |
} | |
/* | |
* Internal method to handle both chaincode calls | |
*/ | |
async _sendChaincodeProposal(request, command, timeout) { | |
let errorMsg = null; | |
// validate the incoming request | |
if (!errorMsg) { | |
errorMsg = client_utils.checkProposalRequest(request, true); | |
} | |
if (!errorMsg) { | |
errorMsg = client_utils.checkInstallRequest(request); | |
} | |
if (errorMsg) { | |
console.log('sendChainCodeProposal error ' + errorMsg); | |
throw new Error(errorMsg); | |
} | |
const peers = this._getTargets(request.targets, Constants.NetworkConfig.ENDORSING_PEER_ROLE); | |
// args is optional because some chaincode may not need any input parameters during initialization | |
if (!request.args) { | |
request.args = []; | |
} | |
// step 1: construct a ChaincodeSpec | |
const args = []; | |
args.push(Buffer.from(request.fcn ? request.fcn : 'init', 'utf8')); | |
for (const arg of request.args) { | |
args.push(Buffer.from(arg, 'utf8')); | |
} | |
const ccSpec = { | |
type: client_utils.translateCCType(request.chaincodeType), | |
chaincode_id: { | |
name: request.chaincodeId, | |
version: request.chaincodeVersion | |
}, | |
input: { | |
args: args | |
} | |
}; | |
// step 2: construct the ChaincodeDeploymentSpec | |
const chaincodeDeploymentSpec = new _ccProto.ChaincodeDeploymentSpec(); | |
chaincodeDeploymentSpec.setChaincodeSpec(ccSpec); | |
const signer = this._clientContext._getSigningIdentity(request.txId.isAdmin()); | |
/** | |
* lcccSpec_args: | |
* args[0] is the command | |
* args[1] is the channel name | |
* args[2] is the ChaincodeDeploymentSpec | |
* | |
* the following optional arguments here (they can each be nil and may or may not be present) | |
* args[3] is a marshaled SignaturePolicyEnvelope representing the endorsement policy | |
* args[4] is the name of escc | |
* args[5] is the name of vscc | |
* args[6] is a marshaled CollectionConfigPackage struct | |
*/ | |
const lcccSpec_args = [ | |
Buffer.from(command), | |
Buffer.from(this._name), | |
chaincodeDeploymentSpec.toBuffer(), | |
Buffer.from(''), | |
Buffer.from(''), | |
Buffer.from(''), | |
]; | |
if (request['endorsement-policy']) { | |
lcccSpec_args[3] = this._buildEndorsementPolicy(request['endorsement-policy']); | |
} | |
if (request['collections-config']) { | |
const collectionConfigPackage = CollectionConfig.buildCollectionConfigPackage(request['collections-config']); | |
lcccSpec_args[6] = collectionConfigPackage.toBuffer(); | |
} | |
const lcccSpec = { | |
// type: _ccProto.ChaincodeSpec.Type.GOLANG, | |
type: client_utils.translateCCType(request.chaincodeType), | |
chaincode_id: {name: Constants.LSCC}, | |
input: {args: lcccSpec_args} | |
}; | |
const channelHeader = client_utils.buildChannelHeader( | |
_commonProto.HeaderType.ENDORSER_TRANSACTION, | |
this._name, | |
request.txId.getTransactionID(), | |
null, | |
Constants.LSCC, | |
client_utils.buildCurrentTimestamp(), | |
this._clientContext.getClientCertHash() | |
); | |
const header = client_utils.buildHeader(signer, channelHeader, request.txId.getNonce()); | |
const proposal = client_utils.buildProposal(lcccSpec, header, request.transientMap); | |
const signed_proposal = client_utils.signProposal(signer, proposal); | |
const responses = await client_utils.sendPeersProposal(peers, signed_proposal, timeout); | |
return [responses, proposal]; | |
} | |
/** | |
* @typedef {Object} ChaincodeInvokeRequest | |
* This object contains many properties that will be used by the Discovery service. | |
* @property {Peer[] | string[]} targets - Optional. The peers that will receive this request, | |
* when not provided the list of peers added to this channel object will | |
* be used. When this channel has been initialized using the discovery | |
* service the proposal will be sent to the peers on the list provided | |
* discovery service if no targets are specified. | |
* @property {string} chaincodeId - Required. The id of the chaincode to process | |
* the transaction proposal | |
* @property {DiscoveryChaincodeIntereset} endorsement_hint - Optional. A | |
* of {@link DiscoveryChaincodeInterest} object that will be used by | |
* discovery service to calculate an appropriate endorsement plan. | |
* The parameter is only required when the endorsement will be preformed | |
* by a chaincode that will call other chaincodes or if the endorsement | |
* should be made by only peers within a collection or collections. | |
* @property {TransactionID} txId - Optional. TransactionID object with the | |
* transaction id and nonce. txId is required for [sendTransactionProposal]{@link Channel#sendTransactionProposal} | |
* and optional for [generateUnsignedProposal]{@link Channel#generateUnsignedProposal} | |
* @property {object} [transientMap] - Optional. Object with String property names | |
* and Buffer property values that can be used by the chaincode but not | |
* saved in the ledger. Data such as cryptographic information for | |
* encryption can be passed to the chaincode using this technique. Data | |
* that is to be kept in a private data store (a collection) should be | |
* passed to the chaincode in the transientMap. | |
* @property {string} fcn - Optional. The function name to be returned when | |
* calling <code>stub.GetFunctionAndParameters()</code> | |
* in the target chaincode. Default is 'invoke' | |
* @property {string[]} args - An array of string arguments specific to the | |
* chaincode's 'Invoke' method | |
* @property {string[]} required - Optional. An array of strings that represent | |
* the names of peers that are required for the endorsement. These will | |
* be the only peers which the proposal will be sent. | |
* This list only applies to endorsements using the discovery service. | |
* This property is used by the {@link DiscoveryEndorsementHandler}. | |
* @property {string[]} ignore - Optional. An array of strings that represent | |
* the names of peers that should be ignored by the endorsement. | |
* This list only applies to endorsements using the discovery service. | |
* This property is used by the {@link DiscoveryEndorsementHandler}. | |
* @property {string[]} preferred - Optional. An array of strings that represent | |
* the names of peers that should be given priority by the endorsement. | |
* Priority means that these peers will be chosen first for endorsements | |
* when an endorsement plan has more peers in a group then needed to | |
* satisfy the endorsement policy. | |
* This list only applies to endorsements using the discovery service. | |
* This property is used by the {@link DiscoveryEndorsementHandler}. | |
* @property {string[]} requiredOrgs - Optional. An array of strings that represent | |
* the names of an organization's MSP id that are required for the | |
* endorsement. Only peers in these organizations will be sent the | |
* proposal. | |
* This list only applies to endorsements using the discovery service. | |
* This property is used by the {@link DiscoveryEndorsementHandler}. | |
* @property {string[]} ignoreOrgs - Optional. An array of strings that represent | |
* the names of an organization's MSP id that should be ignored by the | |
* endorsement. | |
* This list only applies to endorsements using the discovery service. | |
* This property is used by the {@link DiscoveryEndorsementHandler}. | |
* @property {string[]} preferredOrgs - Optional. An array of strings that represent | |
* the names of an organization's MSP id that should be given priority | |
* by the endorsement. Peers within an organization may have their | |
* ledger height considered using the optional property {@link preferredHeightGap} | |
* before being added to the priority list. | |
* This list only applies to endorsements using the discovery service. | |
* This property is used by the {@link DiscoveryEndorsementHandler}. | |
* @property {Number} preferredHeightGap - Optional. An integer representing | |
* the maximum difference in the block height of a peer and the | |
* highest block height found in a group of peers within an endorsement | |
* plan allowed to be a preferred peer. A peer will not be given | |
* priority if it's block height is less than the highest block | |
* height by an amount greater than this value. There is no default, | |
* if this value is not provided the block height of the peer will | |
* not be considered when being added to the preferred list. | |
* This list only applies to endorsements using the discovery service. | |
* This property is used by the {@link DiscoveryEndorsementHandler}. | |
* @property {string} sort - Optional. A string value that indicates how | |
* the peers within groups should be chosen. | |
* "ledgerHeight", sort the peers descending by the number of blocks | |
* on the channel ledger. | |
* "random", pull the peers randomly from the list, the preferred | |
* will be pulled first. | |
* The default will be to sort by ledger height. | |
*/ | |
/** | |
* Sends a transaction proposal to one or more endorsing peers. | |
* | |
* After a chaincode gets [installed]{@link Client#installChaincode} and | |
* [instantiated]{@link Channel#instantiateChaincode}, it's ready to take endorsement | |
* proposals and participating in transaction processing. A chaincode transaction | |
* starts with a proposal that gets sent to the endorsing peers, which executes | |
* the target chaincode and decides whether the proposal should be endorsed (if it | |
* executes successfully) or not (if the chaincode returns an error). | |
* | |
* @param {ChaincodeInvokeRequest} request | |
* @param {Number} timeout - A number indicating milliseconds to wait on the | |
* response before rejecting the promise with a timeout error. This | |
* overrides the default timeout of the Peer instance and the global | |
* timeout in the config settings. | |
* @returns {Promise} A Promise for the {@link ProposalResponseObject} | |
*/ | |
async sendTransactionProposal(request, timeout) { | |
const method = 'sendTransactionProposal'; | |
console.log('%s - start', method); | |
const errorMsg = client_utils.checkProposalRequest(request, true); | |
if (errorMsg) { | |
logAndThrow(method, errorMsg); | |
} | |
if (!request.args) { | |
// args is not optional because we need for transaction to execute | |
logAndThrow(method, 'Missing "args" in Transaction proposal request'); | |
} | |
if (!request.targets && this._endorsement_handler) { | |
console.log('%s - running with endorsement handler', method); | |
const proposal = Channel._buildSignedProposal(request, this._name, this._clientContext); | |
let endorsement_hint = request.endorsement_hint; | |
if (!endorsement_hint && request.chaincodeId) { | |
endorsement_hint = this._buildDiscoveryInterest(request.chaincodeId); | |
} | |
console.log('%s - endorse with hint %j', method, endorsement_hint); | |
const params = { | |
request: request, | |
signed_proposal: proposal.signed, | |
timeout: timeout, | |
endorsement_hint: endorsement_hint | |
}; | |
const responses = await this._endorsement_handler.endorse(params); | |
return [responses, proposal.source]; | |
} else { | |
console.log('%s - running without endorsement handler', method); | |
request.targets = this._getTargets(request.targets, Constants.NetworkConfig.ENDORSING_PEER_ROLE); | |
return Channel.sendTransactionProposal(request, this._name, this._clientContext, timeout); | |
} | |
} | |
/* | |
* Internal static method to allow transaction proposals to be called without | |
* creating a new channel | |
*/ | |
static async sendTransactionProposal(request, channelId, client_context, timeout) { | |
const method = 'sendTransactionProposal(static)'; | |
console.log('%s - start', method); | |
let errorMsg = client_utils.checkProposalRequest(request, true); | |
if (errorMsg) { | |
// do nothing so we skip the rest of the checks | |
} else if (!request.args) { | |
// args is not optional because we need for transaction to execute | |
errorMsg = 'Missing "args" in Transaction proposal request'; | |
} else if (!request.targets || request.targets.length < 1) { | |
errorMsg = 'Missing peer objects in Transaction proposal'; | |
} | |
if (errorMsg) { | |
console.log('%s error %s', method, errorMsg); | |
throw new Error(errorMsg); | |
} | |
const proposal = Channel._buildSignedProposal(request, channelId, client_context); | |
const responses = await client_utils.sendPeersProposal(request.targets, proposal.signed, timeout); | |
return [responses, proposal.source]; | |
} | |
static _buildSignedProposal(request, channelId, client_context) { | |
const method = '_buildSignedProposal'; | |
console.log('%s - start', method); | |
const args = []; | |
args.push(Buffer.from(request.fcn ? request.fcn : 'invoke', 'utf8')); | |
console.log('%s - adding function arg:%s', method, request.fcn ? request.fcn : 'invoke'); | |
for (let i = 0; i < request.args.length; i++) { | |
console.log('%s - adding arg', method); | |
args.push(Buffer.from(request.args[i], 'utf8')); | |
} | |
// special case to support the bytes argument of the query by hash | |
if (request.argbytes) { | |
console.log('%s - adding the argument :: argbytes', method); | |
args.push(request.argbytes); | |
} else { | |
console.log('%s - not adding the argument :: argbytes', method); | |
} | |
console.log('%s - chaincode ID:%s', method, request.chaincodeId); | |
const invokeSpec = { | |
type: _ccProto.ChaincodeSpec.Type.GOLANG, | |
chaincode_id: {name: request.chaincodeId}, | |
input: {args: args} | |
}; | |
let signer = null; | |
if (request.signer) { | |
signer = request.signer; | |
} else { | |
signer = client_context._getSigningIdentity(request.txId.isAdmin()); | |
} | |
const channelHeader = client_utils.buildChannelHeader( | |
_commonProto.HeaderType.ENDORSER_TRANSACTION, | |
channelId, | |
request.txId.getTransactionID(), | |
null, | |
request.chaincodeId, | |
client_utils.buildCurrentTimestamp(), | |
client_context.getClientCertHash() | |
); | |
const header = client_utils.buildHeader(signer, channelHeader, request.txId.getNonce()); | |
const proposal = client_utils.buildProposal(invokeSpec, header, request.transientMap); | |
const signed_proposal = client_utils.signProposal(signer, proposal); | |
return {signed: signed_proposal, source: proposal}; | |
} | |
/** | |
* @typedef {Object} TransactionRequest | |
* @property {ProposalResponse[]} proposalResponses - An array of or a single | |
* {@link ProposalResponse} object containing the response from the | |
* [endorsement]{@link Channel#sendTransactionProposal} call | |
* @property {Proposal} proposal - A Proposal object containing the original | |
* request for endorsement(s) | |
* @property {TransactionId} txID - Optional. - Must be the transaction ID object | |
* used in the proposal endorsement. The transactionID will | |
* only be used to determine if the signing of the request | |
* should be done by the admin identity or the user assigned | |
* to the client instance. | |
* @property {Orderer|string} orderer - Optional. The orderer instance or string name | |
* of the orderer to operate. See {@link Client.getTargetOrderer} | |
*/ | |
/** | |
* Send the proposal responses that contain the endorsements of a transaction proposal | |
* to the orderer for further processing. This is the 2nd phase of the transaction | |
* lifecycle in the fabric. The orderer will globally order the transactions in the | |
* context of this channel and deliver the resulting blocks to the committing peers for | |
* validation against the chaincode's endorsement policy. When the committering peers | |
* successfully validate the transactions, it will mark the transaction as valid inside | |
* the block. After all transactions in a block have been validated, and marked either as | |
* valid or invalid (with a [reason code]{@link https://github.com/hyperledger/fabric/blob/v1.0.0/protos/peer/transaction.proto#L125}), | |
* the block will be appended (committed) to the channel's ledger on the peer. | |
* <br><br> | |
* The caller of this method must use the proposal responses returned from the endorser along | |
* with the original proposal that was sent to the endorser. Both of these objects are contained | |
* in the {@link ProposalResponseObject} returned by calls to any of the following methods: | |
* <li>[installChaincode()]{@link Client#installChaincode} | |
* <li>[sendInstantiateProposal()]{@link Channel#sendInstantiateProposal} | |
* <li>[sendUpgradeProposal()]{@link Channel#sendUpgradeProposal} | |
* <li>[sendTransactionProposal()]{@link Channel#sendTransactionProposal} | |
* | |
* @param {TransactionRequest} request - {@link TransactionRequest} | |
* @param {Number} timeout - A number indicating milliseconds to wait on the | |
* response before rejecting the promise with a timeout error. This | |
* overrides the default timeout of the Orderer instance and the global | |
* timeout in the config settings. | |
* @returns {Promise} A Promise for a "BroadcastResponse" message returned by | |
* the orderer that contains a single "status" field for a | |
* standard [HTTP response code]{@link https://github.com/hyperledger/fabric/blob/v1.0.0/protos/common/common.proto#L27}. | |
* This will be an acknowledgement from the orderer of a successfully | |
* submitted transaction. | |
*/ | |
async sendTransaction(request, timeout) { | |
console.log('sendTransaction - start :: channel %s', this); | |
if (!request) { | |
throw Error('Missing input request object on the transaction request'); | |
} | |
// Verify that data is being passed in | |
if (!request.proposalResponses) { | |
throw Error('Missing "proposalResponses" parameter in transaction request'); | |
} | |
if (!request.proposal) { | |
throw Error('Missing "proposal" parameter in transaction request'); | |
} | |
let proposalResponses = request.proposalResponses; | |
const chaincodeProposal = request.proposal; | |
const endorsements = []; | |
if (!Array.isArray(proposalResponses)) { | |
// convert to array | |
proposalResponses = [proposalResponses]; | |
} | |
for (const proposalResponse of proposalResponses) { | |
// make sure only take the valid responses to set on the consolidated response object | |
// to use in the transaction object | |
if (proposalResponse && proposalResponse.response && proposalResponse.response.status === 200) { | |
endorsements.push(proposalResponse.endorsement); | |
} | |
} | |
if (endorsements.length < 1) { | |
console.log('sendTransaction - no valid endorsements found'); | |
throw new Error('no valid endorsements found'); | |
} | |
const proposalResponse = proposalResponses[0]; | |
let use_admin_signer = false; | |
if (request.txId) { | |
use_admin_signer = request.txId.isAdmin(); | |
} | |
const envelope = Channel.buildEnvelope(this._clientContext, chaincodeProposal, endorsements, proposalResponse, use_admin_signer); | |
if (this._commit_handler) { | |
const params = { | |
signed_envelope: envelope, | |
request: request, | |
timeout: timeout | |
}; | |
return this._commit_handler.commit(params); | |
} else { | |
// verify that we have an orderer configured | |
const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name); | |
return orderer.sendBroadcast(envelope, timeout); | |
} | |
} | |
/** | |
* @typedef {Object} ProposalRequest | |
* @property {string} fcn - Required. The function name. | |
* @property {string[]} args - Required. Arguments to send to chaincode. | |
* @property {string} chaincodeId - Required. ChaincodeId. | |
* @property {Buffer} argbytes - Optional. Include when an argument must be included as bytes. | |
* @property {object} [transientMap] - Optional. Object with String property names | |
* and Buffer property values that can be used by the chaincode but not | |
* saved in the ledger. Data such as cryptographic information for | |
* encryption can be passed to the chaincode using this technique. Data | |
* that is to be kept in a private data store (a collection) should be | |
* passed to the chaincode in the transientMap. | |
*/ | |
/** | |
* Generates the endorse proposal bytes for a transaction | |
* | |
* Current the [sendTransactionProposal]{@link Channel#sendTransactionProposal} | |
* sign a transaction using the user identity from SDK's context (which | |
* contains the user's private key). | |
* | |
* This method is designed to build the proposal bytes at SDK side, | |
* and user can sign this proposal with their private key, and send | |
* the signed proposal to peer by [sendSignedProposal] | |
* | |
* so the user's private | |
* key would not be required at SDK side. | |
* | |
* @param {ProposalRequest} request chaincode invoke request | |
* @param {string} mspId the mspId for this identity | |
* @param {string} certificate PEM encoded certificate | |
* @param {boolean} admin if this transaction is invoked by admin | |
* @returns {Proposal} | |
*/ | |
generateUnsignedProposal(request, mspId, certificate, admin) { | |
const method = 'generateUnsignedProposal'; | |
console.log('%s - start', method); | |
// check request && request.chaincodeId | |
const errorMsg = client_utils.checkProposalRequest(request, false); | |
if (errorMsg) { | |
logAndThrow(method, errorMsg); | |
} | |
if (!Array.isArray(request.args)) { | |
logAndThrow(method, 'Parameter "args" in transaction proposal request must be an array but was ' + typeof request.args); | |
} | |
const functionName = request.fcn ? request.fcn : 'invoke'; | |
console.log('%s - adding function arg: %s', method, functionName); | |
const args = [Buffer.from(functionName, 'utf8')]; | |
request.args.forEach(arg => { | |
console.log('%s - adding arg %s', method, arg); | |
args.push(Buffer.from(arg, 'utf8')); | |
}); | |
// special case to support the bytes argument of the query by hash | |
if (request.argbytes) { | |
console.log('%s - adding the argument :: argbytes', method); | |
args.push(request.argbytes); | |
} else { | |
console.log('%s - not adding the argument :: argbytes', method); | |
} | |
const invokeSpec = { | |
type: _ccProto.ChaincodeSpec.Type.GOLANG, | |
chaincode_id: {name: request.chaincodeId}, | |
input: {args} | |
}; | |
// certificate, publicKey, mspId, cryptoSuite | |
const identity = new Identity(certificate, null, mspId); | |
const txId = new TransactionID(identity, admin); | |
const channelHeader = client_utils.buildChannelHeader( | |
_commonProto.HeaderType.ENDORSER_TRANSACTION, | |
this._name, | |
txId.getTransactionID(), | |
null, | |
request.chaincodeId, | |
client_utils.buildCurrentTimestamp(), | |
this._clientContext.getClientCertHash() | |
); | |
const header = client_utils.buildHeader(identity, channelHeader, txId.getNonce()); | |
const proposal = client_utils.buildProposal(invokeSpec, header, request.transientMap); | |
return {proposal, txId}; | |
} | |
/** | |
* @typedef {Object} SignedProposal | |
* @property {Peer[]} targets - Required. The function name. | |
* @property {Buffer} signedProposal - Required. The signed endorse proposal | |
*/ | |
/** | |
* Send signed transaction proposal to peer | |
* | |
* @param {SignedProposal} request signed endorse transaction proposal, this signed | |
* proposal would be send to peer directly. | |
* @param {number} timeout the timeout setting passed on sendSignedProposal | |
*/ | |
async sendSignedProposal(request, timeout) { | |
return Channel.sendSignedProposal(request, timeout); | |
} | |
/** | |
* Send signed transaction proposal to peer | |
* | |
* @param {SignedProposal} request signed endorse transaction proposal, this signed | |
* proposal would be send to peer directly. | |
* @param {number} timeout the timeout setting passed on sendSignedProposal | |
*/ | |
static async sendSignedProposal(request, timeout) { | |
const responses = await client_utils.sendPeersProposal(request.targets, request.signedProposal, timeout); | |
return responses; | |
} | |
/** | |
* generate the commit proposal for a transaction | |
* | |
* @param {TransactionRequest} request | |
*/ | |
generateUnsignedTransaction(request) { | |
console.log('generateUnsignedTransaction - start :: channel %s', this._name); | |
if (!request) { | |
throw Error('Missing input request object on the generateUnsignedTransaction() call'); | |
} | |
// Verify that data is being passed in | |
if (!Array.isArray(request.proposalResponses)) { | |
throw Error('"proposalResponses" parameter in transaction request must be an array but was ' + typeof request.proposalResponses); | |
} | |
if (!request.proposal) { | |
throw Error('Missing "proposal" parameter in transaction request'); | |
} | |
const proposalResponses = request.proposalResponses; | |
const chaincodeProposal = request.proposal; | |
const endorsements = []; | |
for (const proposalResponse of proposalResponses) { | |
// make sure only take the valid responses to set on the consolidated response object | |
// to use in the transaction object | |
if (proposalResponse && proposalResponse.response && proposalResponse.response.status === 200) { | |
endorsements.push(proposalResponse.endorsement); | |
} | |
} | |
if (endorsements.length < 1) { | |
console.log('sendTransaction - no valid endorsements found'); | |
throw new Error('no valid endorsements found'); | |
} | |
const proposalResponse = proposalResponses[0]; | |
const proposal = ChannelHelper.buildTransactionProposal( | |
chaincodeProposal, | |
endorsements, | |
proposalResponse | |
); | |
return proposal; | |
} | |
/** | |
* @typedef {Object} SignedCommitProposal | |
* @property {TransactionRequest} request - Required. The commit request | |
* @property {Buffer} signedTransaction - Required. The signed transaction | |
* @property {Orderer|string} orderer - Optional. The orderer instance or string name | |
* of the orderer to operate. See {@link Client.getTargetOrderer} | |
*/ | |
/** | |
* send the signed commit proposal for a transaction | |
* | |
* @param {SignedCommitProposal} request the signed commit proposal | |
* @param {number} timeout the timeout setting passed on sendSignedProposal | |
*/ | |
async sendSignedTransaction(request, timeout) { | |
const signed_envelope = client_utils.toEnvelope(request.signedProposal); | |
if (this._commit_handler) { | |
const params = { | |
signed_envelope, | |
request: request.request, | |
timeout: timeout | |
}; | |
return this._commit_handler.commit(params); | |
} else { | |
// verify that we have an orderer configured | |
const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name); | |
return orderer.sendBroadcast(signed_envelope, timeout); | |
} | |
} | |
/* | |
* Internal static method to allow transaction envelop to be built without | |
* creating a new channel | |
*/ | |
static buildEnvelope(clientContext, chaincodeProposal, endorsements, proposalResponse, use_admin_signer) { | |
const header = _commonProto.Header.decode(chaincodeProposal.getHeader()); | |
const chaincodeEndorsedAction = new _transProto.ChaincodeEndorsedAction(); | |
chaincodeEndorsedAction.setProposalResponsePayload(proposalResponse.payload); | |
chaincodeEndorsedAction.setEndorsements(endorsements); | |
const chaincodeActionPayload = new _transProto.ChaincodeActionPayload(); | |
chaincodeActionPayload.setAction(chaincodeEndorsedAction); | |
// the TransientMap field inside the original proposal payload is only meant for the | |
// endorsers to use from inside the chaincode. This must be taken out before sending | |
// to the orderer, otherwise the transaction will be rejected by the validators when | |
// it compares the proposal hash calculated by the endorsers and returned in the | |
// proposal response, which was calculated without the TransientMap | |
const originalChaincodeProposalPayload = _proposalProto.ChaincodeProposalPayload.decode(chaincodeProposal.payload); | |
const chaincodeProposalPayloadNoTrans = new _proposalProto.ChaincodeProposalPayload(); | |
chaincodeProposalPayloadNoTrans.setInput(originalChaincodeProposalPayload.input); // only set the input field, skipping the TransientMap | |
chaincodeActionPayload.setChaincodeProposalPayload(chaincodeProposalPayloadNoTrans.toBuffer()); | |
const transactionAction = new _transProto.TransactionAction(); | |
transactionAction.setHeader(header.getSignatureHeader()); | |
transactionAction.setPayload(chaincodeActionPayload.toBuffer()); | |
const actions = []; | |
actions.push(transactionAction); | |
const transaction = new _transProto.Transaction(); | |
transaction.setActions(actions); | |
const payload = new _commonProto.Payload(); | |
payload.setHeader(header); | |
payload.setData(transaction.toBuffer()); | |
const signer = clientContext._getSigningIdentity(use_admin_signer); | |
return client_utils.toEnvelope(client_utils.signProposal(signer, payload)); | |
} | |
/** | |
* @typedef {Object} ChaincodeQueryRequest | |
* @property {Peer[]} targets - Optional. The peers that will receive this | |
* request, when not provided the list of peers added to this channel | |
* object will be used. | |
* @property {string} chaincodeId - Required. The id of the chaincode to process | |
* the transaction proposal | |
* @property {object} transientMap - Optional. Object with String property names | |
* and Buffer property values that can be used by the chaincode but not | |
* saved in the ledger. Data such as cryptographic information for | |
* encryption can be passed to the chaincode using this technique. Data | |
* that is to be kept in a private data store (a collection) should be | |
* passed to the chaincode in the transientMap. | |
* @property {string} fcn - Optional. The function name to be returned when | |
* calling <code>stub.GetFunctionAndParameters()</code> | |
* in the target chaincode. Default is 'invoke' | |
* @property {string[]} args - An array of string arguments specific to the | |
* chaincode's 'Invoke' method | |
* @property {integer} request_timeout - The timeout value to use for this request | |
* @property {TransactionID} txId Optional. Transaction ID to use for the query. | |
*/ | |
/** | |
* Sends a proposal to one or more endorsing peers that will be handled by the chaincode. | |
* There is no difference in how the endorsing peers process a request | |
* to invoke a chaincode for transaction vs. to invoke a chaincode for query. All requests | |
* will be presented to the target chaincode's 'Invoke' method which must be implemented to | |
* understand from the arguments that this is a query request. The chaincode must also return | |
* results in the byte array format and the caller will have to be able to decode | |
* these results. | |
* | |
* If the request contains a <code>txId</code> property, that transaction ID will be used, and its administrative | |
* privileges will apply. In this case the <code>useAdmin</code> parameter to this function will be ignored. | |
* | |
* @param {ChaincodeQueryRequest} request | |
* @param {boolean} useAdmin - Optional. Indicates that the admin credentials should be used in making | |
* this call. Ignored if the <code>request</code> contains a <code>txId</code> property. | |
* @returns {Promise} A Promise for an array of byte array results returned from the chaincode | |
* on all Endorsing Peers | |
* @example | |
* <caption>Get the list of query results returned by the chaincode</caption> | |
* const responsePayloads = await channel.queryByChaincode(request); | |
* for (let i = 0; i < responsePayloads.length; i++) { | |
* console.log(util.format('Query result from peer [%s]: %s', i, responsePayloads[i].toString('utf8'))); | |
* } | |
*/ | |
async queryByChaincode(request, useAdmin) { | |
console.log('queryByChaincode - start'); | |
if (!request) { | |
throw new Error('Missing request object for this queryByChaincode call.'); | |
} | |
if (request.txId) { | |
useAdmin = request.txId.isAdmin(); | |
} | |
const targets = this._getTargets(request.targets, Constants.NetworkConfig.CHAINCODE_QUERY_ROLE); | |
const signer = this._clientContext._getSigningIdentity(useAdmin); | |
const txId = request.txId || new TransactionID(signer, useAdmin); | |
// make a new request object so we can add in the txId and not change the user's | |
const query_request = { | |
targets: targets, | |
chaincodeId: request.chaincodeId, | |
fcn: request.fcn, | |
args: request.args, | |
transientMap: request.transientMap, | |
txId, | |
signer: signer | |
}; | |
const proposalResults = await Channel.sendTransactionProposal(query_request, this._name, this._clientContext, request.request_timeout); | |
const responses = proposalResults[0]; | |
console.log('queryByChaincode - results received'); | |
if (!responses || !Array.isArray(responses)) { | |
throw new Error('Payload results are missing from the chaincode query'); | |
} | |
const results = []; | |
responses.forEach((response) => { | |
if (response instanceof Error) { | |
results.push(response); | |
} else if (response.response && response.response.payload) { | |
if (response.response.status === 200) { | |
results.push(response.response.payload); | |
} else { | |
if (response.response.message) { | |
results.push(new Error(response.response.message)); | |
} else { | |
results.push(new Error(response)); | |
} | |
} | |
} else { | |
console.log('queryByChaincode - unknown or missing results in query ::' + results); | |
results.push(new Error(response)); | |
} | |
}); | |
return results; | |
} | |
/** | |
* Utility method to verify a single proposal response. It checks the | |
* following aspects: | |
* <li>The endorser's identity belongs to a legitimate MSP of the channel | |
* and can be successfully deserialized | |
* <li>The endorsement signature can be successfully verified with the | |
* endorser's identity certificate | |
* <br><br> | |
* This method requires that the initialize method of this channel object | |
* has been called to load this channel's MSPs. The MSPs will have the | |
* trusted root certificates for this channel. | |
* | |
* @param {ProposalResponse} proposal_response - The endorsement response from the peer, | |
* includes the endorser certificate and signature over the | |
* proposal + endorsement result + endorser certificate. | |
* @returns {boolean} A boolean value of true when both the identity and | |
* the signature are valid, false otherwise. | |
*/ | |
verifyProposalResponse(proposal_response) { | |
console.log('verifyProposalResponse - start'); | |
if (!proposal_response) { | |
throw new Error('Missing proposal response'); | |
} | |
if (proposal_response instanceof Error) { | |
return false; | |
} | |
if (!proposal_response.endorsement) { | |
throw new Error('Parameter must be a ProposalResponse Object'); | |
} | |
const endorsement = proposal_response.endorsement; | |
let identity; | |
const sid = _identityProto.SerializedIdentity.decode(endorsement.endorser); | |
const mspid = sid.getMspid(); | |
console.log('getMSPbyIdentity - found mspid %s', mspid); | |
const msp = this._msp_manager.getMSP(mspid); | |
if (!msp) { | |
throw new Error(util.format('Failed to locate an MSP instance matching the endorser identity\'s organization %s', mspid)); | |
} | |
console.log('verifyProposalResponse - found endorser\'s MSP'); | |
try { | |
identity = msp.deserializeIdentity(endorsement.endorser, false); | |
if (!identity) { | |
throw new Error('Unable to find the endorser identity'); | |
} | |
} catch (error) { | |
console.log('verifyProposalResponse - getting endorser identity failed with: ', error); | |
return false; | |
} | |
try { | |
// see if the identity is trusted | |
if (!identity.isValid()) { | |
console.log('Endorser identity is not valid'); | |
return false; | |
} | |
console.log('verifyProposalResponse - have a valid identity'); | |
// check the signature against the endorser and payload hash | |
const digest = Buffer.concat([proposal_response.payload, endorsement.endorser]); | |
if (!identity.verify(digest, endorsement.signature)) { | |
console.log('Proposal signature is not valid'); | |
return false; | |
} | |
} catch (error) { | |
console.log('verifyProposalResponse - verify failed with: ', error); | |
return false; | |
} | |
console.log('verifyProposalResponse - This endorsement has both a valid identity and valid signature'); | |
return true; | |
} | |
/** | |
* Utility method to examine a set of proposals to check they contain | |
* the same endorsement result write sets. | |
* This will validate that the endorsing peers all agree on the result | |
* of the chaincode execution. | |
* | |
* @param {ProposalResponse[]} The proposal responses from all endorsing peers | |
* @returns {boolean} True when all proposals compare equally, false otherwise. | |
*/ | |
compareProposalResponseResults(proposal_responses) { | |
console.log('compareProposalResponseResults - start'); | |
if (!Array.isArray(proposal_responses)) { | |
throw new Error('proposal_responses must be an array but was ' + typeof proposal_responses); | |
} | |
if (proposal_responses.length === 0) { | |
throw new Error('proposal_responses is empty'); | |
} | |
if (proposal_responses.some((response) => response instanceof Error)) { | |
return false; | |
} | |
const first_one = _getProposalResponseResults(proposal_responses[0]); | |
for (let i = 1; i < proposal_responses.length; i++) { | |
const next_one = _getProposalResponseResults(proposal_responses[i]); | |
if (next_one.equals(first_one)) { | |
console.log('compareProposalResponseResults - read/writes result sets match index=%s', i); | |
} else { | |
console.log('compareProposalResponseResults - read/writes result sets do not match index=%s', i); | |
return false; | |
} | |
} | |
return true; | |
} | |
/* | |
* utility method to decide on the target for queries that only need ledger access | |
*/ | |
_getTargetForQuery(target) { | |
if (Array.isArray(target)) { | |
throw new Error('"target" parameter is an array, but should be a singular peer object' + | |
' ' + 'or peer name according to the common connection profile loaded by the client instance'); | |
} | |
const targets = this._getTargets(target, Constants.NetworkConfig.LEDGER_QUERY_ROLE, true); | |
// only want to query one peer | |
return [targets[0]]; | |
} | |
/* | |
* utility method to decide on the target for queries that only need ledger access. | |
* Returns a {ChannelPeer|Peer}. Throws if none can be found. | |
*/ | |
_getFirstAvailableTarget(target) { | |
const targets = this._getTargets(target, Constants.NetworkConfig.ALL_ROLES, true); | |
return targets[0]; | |
} | |
/* | |
* utility method to decide on the target for discovery | |
*/ | |
_getTargetForDiscovery(target) { | |
const targets = this._getTargets(target, Constants.NetworkConfig.DISCOVERY_ROLE, true); | |
// only want one peer | |
console.log('------- GET DISCOVER z12--- targets l=' + targets.length) | |
for (let o of targets) { | |
console.log('subtarget --- ') | |
console.log(stringify(o)) | |
console.log('subteraget end') | |
} | |
console.log('--------------------------------') | |
return targets[0]; //'peer1-hlf-peer.peers.svc.cluster.local'; | |
} | |
/* | |
* utility method to decide on the targets for requests | |
* Returns an array of one or more {ChannelPeer|Peer}. Throws if no targets are found. | |
*/ | |
_getTargets(request_targets, role, isTarget) { | |
const targets = []; | |
console.log('get channel peres ' + stringify(this._channel_peers)); | |
if (request_targets) { | |
let targetsTemp = request_targets; | |
if (!Array.isArray(request_targets)) { | |
targetsTemp = [request_targets]; | |
} | |
for (const target_peer of targetsTemp) { | |
console.log(' llooop ' + stringify(target_peer)) | |
if (typeof target_peer === 'string') { | |
console.log('targer peer ios stirn'); | |
const channel_peer = this._channel_peers.get(target_peer); | |
console.log('******targer_peer**** '); | |
if (channel_peer) { | |
targets.push(channel_peer.getPeer()); | |
} else { | |
throw new Error(util.format(PEER_NOT_ASSIGNED_MSG, target_peer)); | |
} | |
} else if (target_peer && target_peer.constructor && target_peer.constructor.name === 'Peer') { | |
targets.push(target_peer); | |
} else if (target_peer && target_peer.constructor && target_peer.constructor.name === 'ChannelPeer') { | |
targets.push(target_peer.getPeer()); | |
} else { | |
throw new Error('Target peer is not a valid peer object instance'); | |
} | |
} | |
} else { | |
this._channel_peers.forEach((channel_peer) => { | |
if (channel_peer.isInRole(role)) { | |
targets.push(channel_peer.getPeer()); | |
} | |
}); | |
} | |
if (targets.length === 0) { | |
let target_msg = 'targets'; | |
if (isTarget) { | |
target_msg = 'target'; | |
} | |
if (role === Constants.NetworkConfig.EVENT_SOURCE_ROLE) { | |
target_msg = 'peer'; | |
} | |
throw new Error(util.format('"%s" parameter not specified and no peers' + | |
' ' + 'are set on this Channel instance' + | |
' ' + 'or specfied for this channel in the network ', target_msg)); | |
} | |
return targets; | |
} | |
/* | |
* utility method to decide on the orderer | |
*/ | |
_getOrderer(request_orderer) { | |
let orderer = null; | |
if (request_orderer) { | |
if (typeof request_orderer === 'string') { | |
orderer = this._orderers.get(request_orderer); | |
if (!orderer) { | |
throw new Error(util.format('Orderer %s not assigned to the channel', request_orderer)); | |
} | |
} else if (request_orderer && request_orderer.constructor && request_orderer.constructor.name === 'Orderer') { | |
orderer = request_orderer; | |
} else { | |
throw new Error('Orderer is not a valid orderer object instance'); | |
} | |
} else { | |
const orderers = this.getOrderers(); | |
orderer = orderers[0]; | |
if (!orderer) { | |
throw new Error('No Orderers assigned to this channel'); | |
} | |
} | |
return orderer; | |
} | |
// internal utility method to build chaincode policy | |
_buildEndorsementPolicy(policy) { | |
return Policy.buildPolicy(this.getMSPManager().getMSPs(), policy); | |
} | |
/** | |
* return a printable representation of this channel object | |
*/ | |
toString() { | |
const orderers = []; | |
for (const orderer of this.getOrderers()) { | |
orderers.push(orderer.toString()); | |
} | |
const peers = []; | |
for (const peer of this.getPeers()) { | |
peers.push(peer.toString()); | |
} | |
const state = { | |
name: this._name, | |
orderers: orderers.length > 0 ? orderers : 'N/A', | |
peers: peers.length > 0 ? peers : 'N/A' | |
}; | |
return JSON.stringify(state).toString(); | |
} | |
}; | |
// internal utility method to decode and get the write set | |
// from a proposal response | |
function _getProposalResponseResults(proposal_response) { | |
if (!proposal_response.payload) { | |
throw new Error('Parameter must be a ProposalResponse Object'); | |
} | |
const payload = _responseProto.ProposalResponsePayload.decode(proposal_response.payload); | |
const extension = _proposalProto.ChaincodeAction.decode(payload.extension); | |
// TODO should we check the status of this action | |
console.log('_getWriteSet - chaincode action status:%s message:%s', extension.response.status, extension.response.message); | |
// return a buffer object which has an equals method | |
return extension.results.toBuffer(); | |
} | |
/** | |
* utility method to load in a config group | |
* @param {Object} config_items - holder of values found in the configuration | |
* @param {Object} versions | |
* @param {Object} group - used for recursive calls | |
* @param {string} name - used to help with the recursive calls | |
* @param {string} org - Organizational name | |
* @param {bool} top - to handle the differences in the structure of groups | |
* @see /protos/common/configtx.proto | |
*/ | |
function loadConfigGroup(config_items, versions, group, name, org, top) { | |
console.log('loadConfigGroup - %s - > group:%s', name, org); | |
if (!group) { | |
console.log('loadConfigGroup - %s - no group', name); | |
console.log('loadConfigGroup - %s - < group', name); | |
return; | |
} | |
const isOrderer = (name.indexOf('base.Orderer') > -1); | |
console.log('loadConfigGroup - %s - version %s', name, group.version); | |
console.log('loadConfigGroup - %s - mod policy %s', name, group.mod_policy); | |
let groups = null; | |
if (top) { | |
groups = group.groups; | |
versions.version = group.version; | |
} else { | |
groups = group.value.groups; | |
versions.version = group.value.version; | |
} | |
console.log('loadConfigGroup - %s - >> groups', name); | |
if (groups) { | |
const keys = Object.keys(groups.map); | |
versions.groups = {}; | |
if (keys.length === 0) { | |
console.log('loadConfigGroup - %s - no groups', name); | |
} | |
for (let i = 0; i < keys.length; i++) { | |
const key = keys[i]; | |
console.log('loadConfigGroup - %s - found config group ==> %s', name, key); | |
versions.groups[key] = {}; | |
// The Application group is where config settings are that we want to find | |
loadConfigGroup(config_items, versions.groups[key], groups.map[key], name + '.' + key, key, false); | |
} | |
} else { | |
console.log('loadConfigGroup - %s - no groups', name); | |
} | |
console.log('loadConfigGroup - %s - << groups', name); | |
console.log('loadConfigGroup - %s - >> values', name); | |
let values = null; | |
if (top) { | |
values = group.values; | |
} else { | |
values = group.value.values; | |
} | |
if (values) { | |
versions.values = {}; | |
const keys = Object.keys(values.map); | |
for (let i = 0; i < keys.length; i++) { | |
const key = keys[i]; | |
versions.values[key] = {}; | |
const config_value = values.map[key]; | |
loadConfigValue(config_items, versions.values[key], config_value, name, org, isOrderer); | |
} | |
} else { | |
console.log('loadConfigGroup - %s - no values', name); | |
} | |
console.log('loadConfigGroup - %s - << values', name); | |
console.log('loadConfigGroup - %s - >> policies', name); | |
let policies = null; | |
if (top) { | |
policies = group.policies; | |
} else { | |
policies = group.value.policies; | |
} | |
if (policies) { | |
versions.policies = {}; | |
const keys = Object.keys(policies.map); | |
for (let i = 0; i < keys.length; i++) { | |
const key = keys[i]; | |
versions.policies[key] = {}; | |
const config_policy = policies.map[key]; | |
loadConfigPolicy(config_items, versions.policies[key], config_policy, name, org); | |
} | |
} else { | |
console.log('loadConfigGroup - %s - no policies', name); | |
} | |
console.log('loadConfigGroup - %s - << policies', name); | |
console.log('loadConfigGroup - %s - < group', name); | |
} | |
/** | |
* utility method to load in a config value | |
* @see /protos/common/configtx.proto | |
* @see /protos/msp/mspconfig.proto | |
* @see /protos/orderer/configuration.proto | |
* @see /protos/peer/configuration.proto | |
*/ | |
function loadConfigValue(config_items, versions, config_value, group_name, org, isOrderer) { | |
console.log('loadConfigValue - %s - value name: %s', group_name, config_value.key); | |
console.log('loadConfigValue - %s - version: %s', group_name, config_value.value.version); | |
console.log('loadConfigValue - %s - mod_policy: %s', group_name, config_value.value.mod_policy); | |
versions.version = config_value.value.version; | |
try { | |
switch (config_value.key) { | |
case 'AnchorPeers': { | |
const anchor_peers = _peerConfigurationProto.AnchorPeers.decode(config_value.value.value); | |
console.log('loadConfigValue - %s - AnchorPeers :: %s', group_name, anchor_peers); | |
if (anchor_peers && anchor_peers.anchor_peers) { | |
for (const i in anchor_peers.anchor_peers) { | |
const anchor_peer = { | |
host: anchor_peers.anchor_peers[i].host, | |
port: anchor_peers.anchor_peers[i].port, | |
org: org | |
}; | |
config_items['anchor-peers'].push(anchor_peer); | |
console.log('loadConfigValue - %s - AnchorPeer :: %s:%s:%s', group_name, anchor_peer.host, anchor_peer.port, anchor_peer.org); | |
} | |
} | |
break; | |
} | |
case 'MSP': { | |
const msp_value = _mspConfigProto.MSPConfig.decode(config_value.value.value); | |
console.log('loadConfigValue - %s - MSP found', group_name); | |
if (!isOrderer) { | |
config_items.msps.push(msp_value); | |
} | |
break; | |
} | |
case 'ConsensusType': { | |
const consensus_type = _ordererConfigurationProto.ConsensusType.decode(config_value.value.value); | |
config_items.settings.ConsensusType = consensus_type; | |
console.log('loadConfigValue - %s - Consensus type value :: %s', group_name, consensus_type.type); | |
break; | |
} | |
case 'BatchSize': { | |
const batch_size = _ordererConfigurationProto.BatchSize.decode(config_value.value.value); | |
config_items.settings.BatchSize = batch_size; | |
console.log('loadConfigValue - %s - BatchSize max_message_count :: %s', group_name, batch_size.maxMessageCount); | |
console.log('loadConfigValue - %s - BatchSize absolute_max_bytes :: %s', group_name, batch_size.absoluteMaxBytes); | |
console.log('loadConfigValue - %s - BatchSize preferred_max_bytes :: %s', group_name, batch_size.preferredMaxBytes); | |
break; | |
} | |
case 'BatchTimeout': { | |
const batch_timeout = _ordererConfigurationProto.BatchTimeout.decode(config_value.value.value); | |
config_items.settings.BatchTimeout = batch_timeout; | |
console.log('loadConfigValue - %s - BatchTimeout timeout value :: %s', group_name, batch_timeout.timeout); | |
break; | |
} | |
case 'ChannelRestrictions': { | |
const channel_restrictions = _ordererConfigurationProto.ChannelRestrictions.decode(config_value.value.value); | |
config_items.settings.ChannelRestrictions = channel_restrictions; | |
console.log('loadConfigValue - %s - ChannelRestrictions max_count value :: %s', group_name, channel_restrictions.max_count); | |
break; | |
} | |
case 'ChannelCreationPolicy': { | |
const creation_policy = _policiesProto.Policy.decode(config_value.value.value); | |
loadPolicy(config_items, versions, config_value.key, creation_policy, group_name, org); | |
break; | |
} | |
case 'HashingAlgorithm': { | |
const hashing_algorithm_name = _commonConfigurationProto.HashingAlgorithm.decode(config_value.value.value); | |
config_items.settings.HashingAlgorithm = hashing_algorithm_name; | |
console.log('loadConfigValue - %s - HashingAlgorithm name value :: %s', group_name, hashing_algorithm_name.name); | |
break; | |
} | |
case 'Consortium': { | |
const consortium_algorithm_name = _commonConfigurationProto.Consortium.decode(config_value.value.value); | |
config_items.settings.Consortium = consortium_algorithm_name; | |
console.log('loadConfigValue - %s - Consortium name value :: %s', group_name, consortium_algorithm_name.name); | |
break; | |
} | |
case 'BlockDataHashingStructure': { | |
const blockdata_hashing_structure = _commonConfigurationProto.BlockDataHashingStructure.decode(config_value.value.value); | |
config_items.settings.BlockDataHashingStructure = blockdata_hashing_structure; | |
console.log('loadConfigValue - %s - BlockDataHashingStructure width value :: %s', group_name, blockdata_hashing_structure.width); | |
break; | |
} | |
case 'OrdererAddresses': { | |
const orderer_addresses = _commonConfigurationProto.OrdererAddresses.decode(config_value.value.value); | |
console.log('loadConfigValue - %s - OrdererAddresses addresses value :: %s', group_name, orderer_addresses.addresses); | |
if (orderer_addresses && orderer_addresses.addresses) { | |
for (const address of orderer_addresses.addresses) { | |
config_items.orderers.push(address); | |
} | |
} | |
break; | |
} | |
case 'KafkaBrokers': { | |
const kafka_brokers = _ordererConfigurationProto.KafkaBrokers.decode(config_value.value.value); | |
console.log('loadConfigValue - %s - KafkaBrokers addresses value :: %s', group_name, kafka_brokers.brokers); | |
if (kafka_brokers && kafka_brokers.brokers) { | |
for (const broker of kafka_brokers.brokers) { | |
config_items['kafka-brokers'].push(broker); | |
} | |
} | |
break; | |
} | |
default: | |
console.log('loadConfigValue - %s - value: %s', group_name, config_value.value.value); | |
} | |
} catch (err) { | |
console.log('loadConfigValue - %s - name: %s - *** unable to parse with error :: %s', group_name, config_value.key, err); | |
} | |
} | |
/** | |
* @typedef {Object} ChannelPeerRoles | |
* @property {boolean} endorsingPeer - Optional. This peer may be sent transaction | |
* proposals for endorsements. The peer must have the chaincode installed. | |
* The app can also use this property to decide which peers to send the | |
* chaincode install request. | |
* Default: true | |
* | |
* @property {boolean} chaincodeQuery - Optional. This peer may be sent transaction | |
* proposals meant only as a query. The peer must have the chaincode | |
* installed. The app can also use this property to decide which peers | |
* to send the chaincode install request. | |
* Default: true | |
* | |
* @property {boolean} ledgerQuery - Optional. This peer may be sent query proposals | |
* that do not require chaincodes, like queryBlock(), queryTransaction(), etc. | |
* Default: true | |
* | |
* @property {boolean} eventSource - Optional. This peer may be the target of a | |
* event listener registration? All peers can produce events, but the | |
* appliatiion typically only needs to connect to one. | |
* Default: true | |
* | |
* @property {boolean} discover - Optional. This peer may be the target of service | |
* discovery. | |
* Default: true | |
*/ | |
/** | |
* The ChannelPeer class represents a peer in the target blockchain network on this channel. | |
* | |
* @class | |
*/ | |
const ChannelPeer = class { | |
/** | |
* Construct a ChannelPeer object with the given Peer and opts. | |
* A channel peer object holds channel based references: | |
* MSP ID of the Organization this peer belongs. | |
* {@link Channel} object used to know the channel this peer is interacting. | |
* {@link Peer} object used for interacting with the Hyperledger fabric network. | |
* {@link ChannelEventHub} object used for listening to block changes on the channel. | |
* List of {@link ChannelPeerRoles} to indicate the roles this peer performs on the channel. | |
* | |
* The roles this Peer performs on this channel are indicated with is object. | |
* | |
* @param {string} mspid - The mspid of the organization this peer belongs. | |
* @param {Channel} channel - The Channel instance. | |
* @param {Peer} peer - The Peer instance. | |
* @param {ChannelPeerRoles} roles - The roles for this peer. | |
*/ | |
constructor(mspid, channel, peer, roles) { | |
this._mspid = mspid; | |
if (channel && channel.constructor && channel.constructor.name === 'Channel') { | |
if (peer && peer.constructor && peer.constructor.name === 'Peer') { | |
this._channel = channel; | |
this._name = peer.getName(); | |
this._peer = peer; | |
this._roles = {}; | |
console.log('ChannelPeer.const - url: %s', peer.getUrl()); | |
if (roles && typeof roles === 'object') { | |
this._roles = Object.assign(roles, this._roles); | |
} | |
} else { | |
throw new Error('Missing Peer parameter'); | |
} | |
} else { | |
throw new Error('Missing Channel parameter'); | |
} | |
} | |
/** | |
* Close the associated peer service connections. | |
* <br>see {@link Peer#close} | |
* <br>see {@link ChannelEventHub#close} | |
*/ | |
close() { | |
this._peer.close(); | |
if (this._channel_event_hub) { | |
this._channel_event_hub.close(); | |
} | |
} | |
/** | |
* Get the MSP ID. | |
* | |
* @returns {string} The mspId. | |
*/ | |
getMspid() { | |
return this._mspid; | |
} | |
/** | |
* Get the name. This is a client-side only identifier for this | |
* object. | |
* | |
* @returns {string} The name of the object | |
*/ | |
getName() { | |
return this._name; | |
} | |
/** | |
* Get the URL of this object. | |
* | |
* @returns {string} Get the URL associated with the peer object. | |
*/ | |
getUrl() { | |
return this._peer.getUrl(); | |
} | |
/** | |
* Set a role for this peer. | |
* | |
* @param {string} role - The name of the role | |
* @param {boolean} isIn - The boolean value of does this peer have this role | |
*/ | |
setRole(role, isIn) { | |
this._roles[role] = isIn; | |
} | |
/** | |
* Checks if this peer is in the specified role. | |
* The default is true when the incoming role is not defined. | |
* The default will be true when this peer does not have the role defined. | |
* | |
* @returns {boolean} If this peer has this role. | |
*/ | |
isInRole(role) { | |
if (!role) { | |
throw new Error('Missing "role" parameter'); | |
} else if (typeof this._roles[role] === 'undefined') { | |
return true; | |
} else { | |
return this._roles[role]; | |
} | |
} | |
/** | |
* Checks if this peer is in the specified organization. | |
* The default is true when the incoming organization name is not defined. | |
* The default will be true when this peer does not have the organization name defined. | |
* | |
* @param {string} mspid - The mspid of the organnization | |
* @returns {boolean} If this peer belongs to the organization. | |
*/ | |
isInOrg(mspid) { | |
if (!mspid || !this._mspid) { | |
return true; | |
} else { | |
return mspid === this._mspid; | |
} | |
} | |
/** | |
* Get the channel event hub for this channel peer. The ChannelEventHub instance will | |
* be assigned when using the {@link Channel} newChannelEventHub() method. When using | |
* a common connection profile, the ChannelEventHub will be automatically assigned | |
* on the Channel Peers as they are created and added to the channel. | |
* | |
* @return {ChannelEventHub} - The ChannelEventHub instance associated with this {@link Peer} instance. | |
*/ | |
getChannelEventHub() { | |
if (!this._channel_event_hub) { | |
this._channel_event_hub = new ChannelEventHub(this._channel, this._peer); | |
} | |
return this._channel_event_hub; | |
} | |
/** | |
* Get the Peer instance this ChannelPeer represents on the channel. | |
* | |
* @returns {Peer} The associated Peer instance. | |
*/ | |
getPeer() { | |
return this._peer; | |
} | |
/** | |
* Wrapper method for the associated peer so this object may be used as a {@link Peer} | |
* {@link Peer#sendProposal} | |
*/ | |
sendProposal(proposal, timeout) { | |
return this._peer.sendProposal(proposal, timeout); | |
} | |
/** | |
* Wrapper method for the associated peer so this object may be used as a {@link Peer} | |
* {@link Peer#sendDiscovery} | |
*/ | |
sendDiscovery(request, timeout) { | |
return this._peer.sendDiscovery(request, timeout); | |
} | |
toString() { | |
return this._peer.toString(); | |
} | |
}; // endof ChannelPeer | |
/* | |
* utility method to load in a config policy | |
* @see /protos/common/configtx.proto | |
*/ | |
function loadConfigPolicy(config_items, versions, config_policy, group_name, org) { | |
console.log('loadConfigPolicy - %s - policy name: %s', group_name, config_policy.key); | |
console.log('loadConfigPolicy - %s - version: %s', group_name, config_policy.value.version); | |
console.log('loadConfigPolicy - %s - mod_policy: %s', group_name, config_policy.value.mod_policy); | |
versions.version = config_policy.value.version; | |
loadPolicy(config_items, versions, config_policy.key, config_policy.value.policy, group_name, org); | |
} | |
function loadPolicy(config_items, versions, key, policy, group_name) { | |
try { | |
if (policy.type === _policiesProto.Policy.PolicyType.SIGNATURE) { | |
const signature_policy = _policiesProto.SignaturePolicyEnvelope.decode(policy.policy); | |
console.log('loadPolicy - %s - policy SIGNATURE :: %s %s', group_name, signature_policy.encodeJSON(), decodeSignaturePolicy(signature_policy.getIdentities())); | |
} else if (policy.type === _policiesProto.Policy.PolicyType.IMPLICIT_META) { | |
const implicit_policy = _policiesProto.ImplicitMetaPolicy.decode(policy.value); | |
const rule = ImplicitMetaPolicy_Rule[implicit_policy.getRule()]; | |
console.log('loadPolicy - %s - policy IMPLICIT_META :: %s %s', group_name, rule, implicit_policy.getSubPolicy()); | |
} else { | |
console.log('loadPolicy - Unknown policy type :: %s', policy.type); | |
throw new Error('Unknown Policy type ::' + policy.type); | |
} | |
} catch (err) { | |
console.log('loadPolicy - %s - name: %s - unable to parse policy %s', group_name, key, err); | |
} | |
} | |
function decodeSignaturePolicy(identities) { | |
const results = []; | |
for (const i in identities) { | |
const identity = identities[i]; | |
switch (identity.getPrincipalClassification()) { | |
case _mspPrincipalProto.MSPPrincipal.Classification.ROLE: | |
results.push(_mspPrincipalProto.MSPRole.decode(identity.getPrincipal()).encodeJSON()); | |
} | |
} | |
return results; | |
} | |
function decodeCollectionsConfig(payload) { | |
const configs = []; | |
const queryResponse = _collectionProto.CollectionConfigPackage.decode(payload); | |
queryResponse.config.forEach((config) => { | |
let collectionConfig = { | |
type: config.payload, | |
}; | |
if (config.payload === 'static_collection_config') { | |
const {static_collection_config} = config; | |
const {signature_policy} = static_collection_config.member_orgs_policy; | |
const identities = decodeSignaturePolicy(signature_policy.identities); | |
// delete member_orgs_policy, and use policy to keep consistency with the format in collections-config.json | |
delete static_collection_config.member_orgs_policy; | |
static_collection_config.policy = { | |
identities: identities.map(i => JSON.parse(i)), | |
policy: signature_policy.rule.n_out_of, | |
}; | |
static_collection_config.block_to_live = static_collection_config.block_to_live.toInt(); | |
collectionConfig = Object.assign(collectionConfig, static_collection_config); | |
} else { | |
throw new Error(`Do not support collections config of type "${config.payload}"`); | |
} | |
configs.push(collectionConfig); | |
}); | |
return configs; | |
} | |
module.exports = Channel; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment