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 • 16.1 kB
JavaScript
"use strict";
/*
* Copyright 2018, 2019 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Transaction = void 0;
const query_1 = require("./impl/query/query");
const EventHandlers = require("./impl/event/defaulteventhandlerstrategies");
const Logger = require("./logger");
const util = require("util");
const logger = Logger.getLogger('Transaction');
function getResponsePayload(proposalResponse) {
const validEndorsementResponse = getValidEndorsementResponse(proposalResponse.responses);
if (!validEndorsementResponse) {
const error = newEndorsementError(proposalResponse);
logger.error('%s', error);
throw error;
}
return validEndorsementResponse.response.payload;
}
function getValidEndorsementResponse(endorsementResponses) {
return endorsementResponses.find((endorsementResponse) => endorsementResponse.endorsement);
}
function newEndorsementError(proposalResponse) {
var _a, _b;
const errorInfos = [];
for (const error of proposalResponse.errors) {
const errorInfo = {
peer: (_a = error === null || error === void 0 ? void 0 : error.connection) === null || _a === void 0 ? void 0 : _a.name,
status: 'grpc',
message: error.message
};
errorInfos.push(errorInfo);
}
for (const endorsement of proposalResponse.responses) {
const errorInfo = {
peer: (_b = endorsement === null || endorsement === void 0 ? void 0 : endorsement.connection) === null || _b === void 0 ? void 0 : _b.name,
status: endorsement.response.status,
message: endorsement.response.message
};
errorInfos.push(errorInfo);
}
const messages = ['No valid responses from any peers. Errors:'];
for (const errorInfo of errorInfos) {
messages.push(util.format('peer=%s, status=%s, message=%s', errorInfo.peer, errorInfo.status, errorInfo.message));
}
return new Error(messages.join('\n '));
}
function isInteger(value) {
return Number.isInteger(value);
}
/**
* Represents a specific invocation of a transaction function, and provides
* flexibility over how that transaction is invoked. Applications should
* obtain instances of this class by calling
* [Contract#createTransaction()]{@link module:fabric-network.Contract#createTransaction}.
* <br><br>
* Instances of this class are stateful. A new instance <strong>must</strong>
* be created for each transaction invocation.
* @memberof module:fabric-network
* @hideconstructor
*/
class Transaction {
/**
* Transaction instances should be obtained only by calling
* [Contract.createTransaction()]{@link module:fabric-network.Contract#createTransaction}. This constructor should
* not be used directly.
*/
constructor(contract, name, state) {
var _a;
const method = `constructor[${name}]`;
logger.debug('%s - start', method);
this.name = name;
this.contract = contract;
this.gatewayOptions = contract.gateway.getOptions();
this.eventHandlerStrategyFactory = this.gatewayOptions.eventHandlerOptions.strategy || EventHandlers.NONE;
this.queryHandler = contract.network.queryHandler;
if (!state) {
// Store the returned copy to prevent state being modified by other code before it is used to send proposals
this.identityContext = contract.gateway.identityContext.calculateTransactionId();
}
else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
this.identityContext = contract.gateway.identityContext.clone({
nonce: Buffer.from(state.nonce, 'base64'),
transactionId: state.transactionId,
});
this.endorsingOrgs = state.endorsingOrgs;
this.endorsingPeers = (_a = state.endorsingPeers) === null || _a === void 0 ? void 0 : _a.map(peerName => contract.network.getChannel().getEndorser(peerName)).filter(endorser => endorser !== undefined);
if (state.transientData) {
this.transientMap = {};
for (const [key, value] of Object.entries(state.transientData)) {
this.transientMap[key] = Buffer.from(value, 'base64');
}
}
}
}
/**
* Get the fully qualified name of the transaction function.
* @returns {string} Transaction name.
*/
getName() {
return this.name;
}
/**
* Set transient data that will be passed to the transaction function
* but will not be stored on the ledger. This can be used to pass
* private data to a transaction function.
* @param {Object} transientMap Object with String property names and
* Buffer property values.
* @returns {module:fabric-network.Transaction} This object, to allow function chaining.
*/
setTransient(transientMap) {
const method = `setTransient[${this.name}]`;
logger.debug('%s - start', method);
this.transientMap = transientMap;
return this;
}
/**
* Get the ID that will be used for this transaction invocation.
* @returns {string} A transaction ID.
*/
getTransactionId() {
return this.identityContext.transactionId;
}
/**
* Set the peers that should be used for endorsement when this transaction
* is submitted to the ledger.
* Setting the peers will override the use of discovery and the submit will
* send the proposal to these peers.
* This will override the setEndorsingOrganizations if previously called.
* @param {Endorser[]} peers - Endorsing peers.
* @returns {module:fabric-network.Transaction} This object, to allow function chaining.
*/
setEndorsingPeers(peers) {
const method = `setEndorsingPeers[${this.name}]`;
logger.debug('%s - start', method);
this.endorsingPeers = peers;
this.endorsingOrgs = undefined;
return this;
}
/**
* Set the organizations that should be used for endorsement when this
* transaction is submitted to the ledger.
* Peers that are in the organizations will be used for the endorsement.
* This will override the setEndorsingPeers if previously called. Setting
* the endorsing organizations will not override discovery, however it will
* filter the peers provided by discovery to be those in these organizatons.
* @param {string[]} orgs - Endorsing organizations.
* @returns {module:fabric-network.Transaction} This object, to allow function chaining.
*/
setEndorsingOrganizations(...orgs) {
const method = `setEndorsingOrganizations[${this.name}]`;
logger.debug('%s - start', method);
this.endorsingOrgs = orgs;
this.endorsingPeers = undefined;
return this;
}
/**
* Set an event handling strategy to use for this transaction instead of the default configured on the gateway.
* @param strategy An event handling strategy.
* @returns {module:fabric-network.Transaction} This object, to allow function chaining.
*/
setEventHandler(strategy) {
this.eventHandlerStrategyFactory = strategy;
return this;
}
/**
* 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.
* @async
* @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.
* @throws {module:fabric-network.TransactionError} If the transaction committed with an unsuccessful transaction
* validation code, and so did not update the ledger.
*/
async submit(...args) {
const method = `submit[${this.name}]`;
logger.debug('%s - start', method);
const channel = this.contract.network.getChannel();
const transactionOptions = this.gatewayOptions.eventHandlerOptions;
// This is the object that will centralize this endorsement activities
// with the fabric network
const endorsement = channel.newEndorsement(this.contract.chaincodeId);
const proposalBuildRequest = this.newBuildProposalRequest(args);
logger.debug('%s - build and send the endorsement', method);
// build the outbound request along with getting a new transactionId
// from the identity context
endorsement.build(this.identityContext, proposalBuildRequest);
endorsement.sign(this.identityContext);
// ------- S E N D P R O P O S A L
// This is where the request gets sent to the peers
const proposalSendRequest = {};
if (isInteger(transactionOptions.endorseTimeout)) {
proposalSendRequest.requestTimeout = transactionOptions.endorseTimeout * 1000; // in ms;
}
if (this.endorsingPeers) {
logger.debug('%s - user has assigned targets', method);
proposalSendRequest.targets = this.endorsingPeers;
}
else if (this.contract.network.discoveryService) {
logger.debug('%s - discovery handler will be used for endorsing', method);
proposalSendRequest.handler = await this.contract.getDiscoveryHandler();
if (this.endorsingOrgs) {
logger.debug('%s - using discovery and user has assigned endorsing orgs %s', method, this.endorsingOrgs);
proposalSendRequest.requiredOrgs = this.endorsingOrgs;
}
}
else if (this.endorsingOrgs) {
logger.debug('%s - user has assigned endorsing orgs %s', method, this.endorsingOrgs);
const flatten = (accumulator, value) => {
accumulator.push(...value);
return accumulator;
};
proposalSendRequest.targets = this.endorsingOrgs.map((mspid) => channel.getEndorsers(mspid)).reduce(flatten, []);
}
else {
logger.debug('%s - targets will default to all that are assigned to this channel', method);
proposalSendRequest.targets = channel.getEndorsers();
}
// by now we should have targets or a discovery handler to be used
// by the send() of the proposal instance
const proposalResponse = await endorsement.send(proposalSendRequest);
try {
const result = getResponsePayload(proposalResponse);
// ------- E V E N T M O N I T O R
const eventHandler = this.eventHandlerStrategyFactory(endorsement.getTransactionId(), this.contract.network);
await eventHandler.startListening();
const commit = endorsement.newCommit();
commit.build(this.identityContext);
commit.sign(this.identityContext);
// ----- C O M M I T E N D O R S E M E N T
// this is where the endorsement results are sent to the orderer
const commitSendRequest = {};
if (isInteger(transactionOptions.commitTimeout)) {
commitSendRequest.requestTimeout = transactionOptions.commitTimeout * 1000; // in ms;
}
if (proposalSendRequest.handler) {
logger.debug('%s - use discovery to commit', method);
commitSendRequest.handler = proposalSendRequest.handler;
}
else {
logger.debug('%s - use the orderers assigned to the channel', method);
commitSendRequest.targets = channel.getCommitters();
}
// by now we should have a discovery handler or use the target orderers
// that have been assigned from the channel to perform the commit
const commitResponse = await commit.send(commitSendRequest);
logger.debug('%s - commit response %j', method, commitResponse);
if (commitResponse.status !== 'SUCCESS') {
const msg = `Failed to commit transaction %${endorsement.getTransactionId()}, orderer response status: ${commitResponse.status}`;
logger.error('%s - %s', method, msg);
eventHandler.cancelListening();
throw new Error(msg);
}
else {
logger.debug('%s - successful commit', method);
}
logger.debug('%s - wait for the transaction to be committed on the peer', method);
await eventHandler.waitForEvents();
return result;
}
catch (err) {
err.responses = proposalResponse.responses;
err.errors = proposalResponse.errors;
throw err;
}
}
/**
* Evaluate a transaction function and return its results.
* The transaction function 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.
* @async
* @param {...string} [args] Transaction function arguments.
* @returns {Promise<Buffer>} Payload response from the transaction function.
*/
async evaluate(...args) {
const method = `evaluate[${this.name}]`;
logger.debug('%s - start', method);
const queryProposal = this.contract.network.getChannel().newQuery(this.contract.chaincodeId);
const request = this.newBuildProposalRequest(args);
logger.debug('%s - build and sign the query', method);
queryProposal.build(this.identityContext, request);
queryProposal.sign(this.identityContext);
const query = new query_1.QueryImpl(queryProposal, this.gatewayOptions.queryHandlerOptions);
logger.debug('%s - handler will send', method);
const results = await this.queryHandler.evaluate(query);
logger.debug('%s - queryHandler completed', method);
return results;
}
/**
* Extract the state of this transaction in a form that can be reconstructed using
* [Contract#deserializeTransaction()]{@link module:fabric-network.Contract#deserializeTransaction}. This allows a
* transaction to persisted, and then reconstructed and resubmitted following a client application restart. There is
* no guarantee of compatibility for the serialized data between different versions of this package.
* @returns {Buffer} A serialized transaction.
*/
serialize() {
var _a;
const state = {
name: this.name,
nonce: this.identityContext.nonce.toString('base64'),
transactionId: this.identityContext.transactionId,
endorsingOrgs: this.endorsingOrgs,
endorsingPeers: (_a = this.endorsingPeers) === null || _a === void 0 ? void 0 : _a.map(endorser => endorser.name),
};
if (this.transientMap) {
state.transientData = {};
for (const [key, value] of Object.entries(this.transientMap)) {
state.transientData[key] = value.toString('base64');
}
}
const json = JSON.stringify(state);
return Buffer.from(json);
}
newBuildProposalRequest(args) {
const request = {
fcn: this.name,
args: args,
generateTransactionId: false
};
if (this.transientMap) {
request.transientMap = this.transientMap;
}
return request;
}
}
exports.Transaction = Transaction;
//# sourceMappingURL=transaction.js.map