UNPKG

fabric-network

Version:

SDK for writing node.js applications to interact with Hyperledger Fabric. This package encapsulates the APIs to connect to a Fabric network, submit transactions and perform queries against the ledger.

340 lines • 15.6 kB
"use strict"; /* * Copyright 2018, 2019 IBM All Rights Reserved. * * SPDX-License-Identifier: Apache-2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ContractImpl = void 0; const transaction_1 = require("./transaction"); const contractlistenersession_1 = require("./impl/event/contractlistenersession"); const listenersession_1 = require("./impl/event/listenersession"); const Logger = require("./logger"); const logger = Logger.getLogger('Contract'); const util = require("util"); /** * Ensure transaction name is a non-empty string. * @private * @param {string} name Transaction name. * @throws {Error} if the name is invalid. */ function verifyTransactionName(name) { if (typeof name !== 'string' || name.length === 0) { const msg = util.format('Transaction name must be a non-empty string: %j', name); logger.error('verifyTransactionName:', msg); throw new Error(msg); } } /** * Ensure that, if a namespace is defined, it is a non-empty string * @private * @param {string|undefined} namespace Transaction namespace. * @throws {Error} if the namespace is invalid. */ function verifyNamespace(namespace) { if (namespace && typeof namespace !== 'string') { const msg = util.format('Namespace must be a non-empty string: %j', namespace); logger.error('verifyNamespace:', msg); throw new Error(msg); } } /** * <p>Represents a smart contract (chaincode) instance in a network. * Applications should get a Contract instance using the * networks's [getContract]{@link module:fabric-network.Network#getContract} method.</p> * * <p>The Contract allows applications to:</p> * <ul> * <li>Submit transactions that store state to the ledger using * [submitTransaction]{@link module:fabric-network.Contract#submitTransaction}.</li> * <li>Evaluate transactions that query state from the ledger using * [evaluateTransaction]{@link module:fabric-network.Contract#evaluateTransaction}.</li> * <li>Listen for new chaincode events and replay previous chaincode events emitted by the smart contract using * [addContractListener]{@link module:fabric-network.Contract#addContractListener}.</li> * </ul> * * <p>If more control over transaction invocation is required, such as including transient data, * [createTransaction]{@link module:fabric-network.Contract#createTransaction} can be used to build a transaction * request that is submitted to or evaluated by the smart contract.</p> * @interface Contract * @memberof module:fabric-network */ /** * Create an object representing a specific invocation of a transaction * function implemented by this contract, and provides more control over * the transaction invocation. A new transaction object <strong>must</strong> * be created for each transaction invocation. * @method Contract#createTransaction * @memberof module:fabric-network * @param {string} name Transaction function name. * @returns {module:fabric-network.Transaction} A transaction object. */ /** * Deserialize a transaction from previously saved state. * @method Contract#deserializeTransaction * @memberof module:fabric-network * @param {Buffer} data Serialized transaction data. * @return {module:fabric-network.Transaction} A transaction object. */ /** * Submit a transaction to the ledger. The transaction function <code>name</code> * will be evaluated on the endorsing peers and then submitted to the ordering service * for committing to the ledger. * This function is equivalent to calling <code>createTransaction(name).submit()</code>. * @method Contract#submitTransaction * @memberof module:fabric-network * @param {string} name Transaction function name. * @param {...string} [args] Transaction function arguments. * @returns {Buffer} Payload response from the transaction function. * @throws {module:fabric-network.TimeoutError} If the transaction was successfully submitted to the orderer but * timed out before a commit event was received from peers. */ /** * Evaluate a transaction function and return its results. * The transaction function <code>name</code> * will be evaluated on the endorsing peers but the responses will not be sent to * the ordering service and hence will not be committed to the ledger. * This is used for querying the world state. * This function is equivalent to calling <code>createTransaction(name).evaluate()</code>. * @method Contract#evaluateTransaction * @memberof module:fabric-network * @param {string} name Transaction function name. * @param {...string} [args] Transaction function arguments. * @returns {Buffer} Payload response from the transaction function. */ /** * Add a listener to receive all chaincode events emitted by the smart contract as part of successfully committed * transactions. The default is to listen for full contract events from the current block position. * @method Contract#addContractListener * @memberof module:fabric-network * @param {module:fabric-network.ContractListener} listener A contract listener callback function. * @param {module:fabric-network.ListenerOptions} [options] Listener options. * @returns {Promise<module:fabric-network.ContractListener>} The added listener. * @example * const listener: ContractListener = async (event) => { * if (event.eventName === 'newOrder') { * const details = event.payload.toString('utf8'); * // Run business process to handle orders * } * }; * contract.addContractListener(listener); */ /** * Remove a previously added contract listener. * @method Contract#removeContractListener * @memberof module:fabric-network * @param {module:fabric-network.ContractListener} listener A contract listener callback function. */ /** * Provide a Discovery Interest settings to help the peer's discovery service * build an endorsement plan. This chaincode Id will be include by default in * the list of discovery interests. If this contract's chaincode is in one or * more collections then use this method with this chaincode Id to change the * default discovery interest to include those collection names. * @method Contract#addDiscoveryInterest * @memberof module:fabric-network * @param {DiscoveryInterest} interest - These will be added to the existing discovery interests and used when * {@link module:fabric-network.Transaction#submit} is called. * @return {Contract} This Contract instance */ /** * reset Discovery interest to default of this contracts chaincode name * and no collection names and no other chaincode names. * @method Contract#resetDiscoveryInterests * @memberof module:fabric-network * @return {Contract} This Contract instance */ /** * Retrieve the Discovery Interest settings that will help the peer's * discovery service build an endorsement plan. * @method Contract#getDiscoveryInterests * @memberof module:fabric-network * @return {DiscoveryInterest[]} - An array of DiscoveryInterest */ /** * A callback function that will be invoked when a block event is received. * @callback ContractListener * @memberof module:fabric-network * @async * @param {module:fabric-network.ContractEvent} event Contract event. * @returns {Promise<void>} */ class ContractImpl { constructor(network, chaincodeId, namespace) { this.contractListeners = new Map(); this.discoveryResultsListeners = new Array(); const method = `constructor[${namespace}]`; logger.debug('%s - start', method); verifyNamespace(namespace); this.network = network; this.chaincodeId = chaincodeId; this.gateway = network.getGateway(); this.namespace = namespace; this.contractListeners = new Map(); this.discoveryInterests = [{ name: chaincodeId }]; } createTransaction(name) { verifyTransactionName(name); const qualifiedName = this._getQualifiedName(name); const transaction = new transaction_1.Transaction(this, qualifiedName); return transaction; } deserializeTransaction(data) { const state = JSON.parse(data.toString()); return new transaction_1.Transaction(this, state.name, state); } async submitTransaction(name, ...args) { return this.createTransaction(name).submit(...args); } async evaluateTransaction(name, ...args) { return this.createTransaction(name).evaluate(...args); } async addContractListener(listener, options) { const sessionSupplier = () => Promise.resolve(new contractlistenersession_1.ContractListenerSession(listener, this.chaincodeId, this.network, options)); const contractListener = await listenersession_1.addListener(listener, this.contractListeners, sessionSupplier); return contractListener; } removeContractListener(listener) { listenersession_1.removeListener(listener, this.contractListeners); } /** * Internal use * Use this method to get the DiscoveryHandler to get the endorsements * needed to commit a transaction. * The first time this method is called, this contract's DiscoveryService * instance will be setup. * The service will make a discovery request to the same * target as that used by the Network. The request will include this contract's * discovery interests. This will enable the peer's discovery * service to generate an endorsement plan based on the chaincode's * endorsement policy, the collection configuration, and the current active * peers. * Note: It is assumed that the discovery interests will not * change on successive calls. The handler's DiscoveryService will use the * "refreshAge" discovery option after the first call to determine if the * endorsement plan should be refreshed by a new call to the peer's * discovery service. * @private * @return {DiscoveryHandler} The handler that will work with the discovery * endorsement plan to send a proposal to be endorsed to the peers as described * in the plan. */ async getDiscoveryHandler() { const method = `getDiscoveryHandler[${this.chaincodeId}]`; logger.debug('%s - start', method); // Contract is only using discovery if the network is too if (!this.network.discoveryService) { logger.debug('%s - not using discovery - return null handler', method); return undefined; } // check if we have initialized this contract's discovery if (!this.discoveryService) { logger.debug('%s - setting up contract discovery', method); this.discoveryService = this.network.getChannel().newDiscoveryService(this.chaincodeId); const targets = this.network.discoveryService.targets; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const idx = this.gateway.identityContext; const asLocalhost = this.gateway.getOptions().discovery.asLocalhost; logger.debug('%s - using discovery interest %j', method, this.discoveryInterests); this.discoveryService.build(idx, { interest: this.discoveryInterests }); this.discoveryService.sign(idx); // go get the endorsement plan from the peer's discovery service // to be ready to be used by the transaction's submit await this.discoveryService.send({ asLocalhost, targets }); logger.debug('%s - endorsement plan retrieved', method); const hasDiscoveryResults = this.discoveryService.hasDiscoveryResults(); this.notifyDiscoveryResultsListeners(hasDiscoveryResults); logger.debug('%s - completed discovery results as first one', method); } else { if (!this.discoveryService.hasDiscoveryResults()) { // maybe the discovery service was created by another submit of // this same contract, make sure it has completed getting the // discovery results, we do not want this submission to also // get the discovery results, just wait for first one to complete. await this.waitDiscoveryResults(); } } // The handler will have access to the endorsement plan fetched // by the parent DiscoveryService instance. logger.debug('%s - returning a new discovery service handler', method); return this.discoveryService.newHandler(); } /* * Internal method to setup a Promise that will wait to be notified when * the discovery service has retreived the discovery results. */ waitDiscoveryResults() { const method = `checkDiscoveryResults[${this.chaincodeId}]`; logger.debug('%s - start', method); return new Promise((resolve, reject) => { const handle = setTimeout(() => { reject(new Error('Timed out waiting for discovery results')); }, 30000); this.registerDiscoveryResultsListener((hasDiscoveryResults) => { clearTimeout(handle); if (hasDiscoveryResults) { logger.debug('%s - discovery results have been retieved', method); resolve(); } else { const error = new Error('Failed to retrieve discovery results'); logger.error('%s - %s', method, error); reject(error); } }); }); } /* * Internal method to register to be notified when * discovery results are ready to be used. */ registerDiscoveryResultsListener(callback) { const method = `registerDiscoveryResultsListener[${this.chaincodeId}]`; logger.debug('%s - start', method); this.discoveryResultsListeners.push(callback); } /* * Interal method to notify all other submits that the discovery * results are now ready to be used. This will have the Promise * resolve and all the other submits to continue to process. */ notifyDiscoveryResultsListeners(hasDiscoveryResults) { const method = `notifyDiscoveryResultsListeners[${this.chaincodeId}]`; logger.debug('%s - start', method); let listener; while ((listener = this.discoveryResultsListeners.pop()) !== undefined) { listener(hasDiscoveryResults); } } addDiscoveryInterest(interest) { const method = `addDiscoveryInterest[${this.chaincodeId}]`; if (typeof interest !== 'object') { throw Error('"interest" parameter must be a DiscoveryInterest object'); } logger.debug('%s - adding %s', method, interest); const existingIndex = this.discoveryInterests.findIndex((entry) => entry.name === interest.name); if (existingIndex >= 0) { this.discoveryInterests[existingIndex] = interest; } else { this.discoveryInterests.push(interest); } return this; } resetDiscoveryInterests() { const method = `resetDiscoveryInterest[${this.chaincodeId}]`; logger.debug('%s - start', method); this.discoveryInterests = [{ name: this.chaincodeId }]; this.discoveryService = undefined; return this; } getDiscoveryInterests() { return this.discoveryInterests; } _getQualifiedName(name) { return (this.namespace ? `${this.namespace}:${name}` : name); } } exports.ContractImpl = ContractImpl; //# sourceMappingURL=contract.js.map