diff --git a/fabric-network/index.js b/fabric-network/index.js index 4f320a307b..0d058ff571 100644 --- a/fabric-network/index.js +++ b/fabric-network/index.js @@ -276,6 +276,7 @@ * @interface BlockEvent * @memberof module:fabric-network * @see module:fabric-network.FilteredBlockEvent + * @see module:fabric-network.FullBlockEvent * @property {module:fabric-network.EventType} type The type of this block event. * @property {Long} blockNumber The number of the block this event represents. */ @@ -305,11 +306,29 @@ */ +/** + * The type for BlockEvent objects with a type of full. + * @interface FullBlockEvent + * @implements {module:fabric-network.BlockEvent} + * @memberof module:fabric-network + * @property {'full'} type The type of this block event. + * @property {Block} blockData The raw block event protobuf. + */ + +/** + * Get the transactions included in this block. + * @method FilteredBlockEvent#getTransactionEvents + * @memberof module:fabric-network + * @returns {module:fabric-network.FullTransactionEvent[]} Transaction events. + */ + + /** * Event representing a transaction processed within a block. * @interface TransactionEvent * @memberof module:fabric-network * @see module:fabric-network.FilteredTransactionEvent + * @see module:fabric-network.FullTransactionEvent * @property {module:fabric-network.EventType} type The type of this transaction event. * @property {string} transactionId The ID of the transaction this event represents. * @property {string} status The status of this transaction. @@ -334,6 +353,7 @@ /** + * The type for TransactionEvent objects with a type of filtered. * @interface FilteredTransactionEvent * @implements {module:fabric-network.TransactionEvent} * @memberof module:fabric-network @@ -350,20 +370,47 @@ /** * Get the contract events emitted by this transaction. - * @method TransactionEvent#getContractEvents + * @method FilteredTransactionEvent#getContractEvents * @memberof module:fabric-network * @returns {module:fabric-network.FilteredContractEvent[]} Contract events. */ +/** + * The type for TransactionEvent objects with a type of full. + * @interface FullTransactionEvent + * @implements {module:fabric-network.TransactionEvent} + * @memberof module:fabric-network + * @property {'full'} type The type of this transaction event. + * @property {any} transactionData The raw transaction event protobuf. + */ + +/** + * Get the parent block event for this event. + * @method FullTransactionEvent#getBlockEvent + * @memberof module:fabric-network + * @returns {module:fabric-network.FullBlockEvent} Transaction event protobuf. + */ + +/** + * Get the contract events emitted by this transaction. + * @method FullTransactionEvent#getContractEvents + * @memberof module:fabric-network + * @returns {module:fabric-network.FullContractEvent[]} Contract events. + */ + + /** * Event representing a contract event emitted by a smart contract. * @interface ContractEvent * @memberof module:fabric-network * @see module:fabric-network.FilteredContractEvent + * @see module:fabric-network.FullContractEvent * @property {module:fabric-network.EventType} type The type of this contract event. * @property {string} chaincodeId The chaincode ID of the smart contract that emitted this event. * @property {string} eventName The name of the emitted event. + * @property {Buffer} [payload] The data associated with this event by the smart contract. Note that filtered events + * do not include any payload data. */ /** @@ -375,21 +422,39 @@ /** - * Event representing a contract event emitted by a smart contract. + * The type for ContractEvent objects with a type of filtered. * @interface FilteredContractEvent * @implements {module:fabric-network.ContractEvent} * @memberof module:fabric-network * @property {'filtered'} type The type of this contract event. + * @property {undefined} payload Filtered events do not incude any payload data. */ /** * Get the parent transaction event of this event. - * @method ContractEvent#getTransactionEvent + * @method FilteredContractEvent#getTransactionEvent * @memberof module:fabric-network * @returns {module:fabric-network.FilteredTransactionEvent} A transaction event. */ +/** + * The type for TransactionEvent objects with a type of full. + * @interface FullContractEvent + * @implements {module:fabric-network.ContractEvent} + * @memberof module:fabric-network + * @property {'full'} type The type of this contract event. + * @property {Buffer} payload The data associated with this event by the smart contract. + */ + +/** + * Get the parent transaction event of this event. + * @method FullContractEvent#getTransactionEvent + * @memberof module:fabric-network + * @returns {module:fabric-network.FullTransactionEvent} A transaction event. + */ + + /** * A callback function that will be invoked when a block event is received. * @callback ContractListener diff --git a/fabric-network/src/contract.js b/fabric-network/src/contract.js index add6b4d8d1..c2fb52303b 100644 --- a/fabric-network/src/contract.js +++ b/fabric-network/src/contract.js @@ -128,14 +128,14 @@ class Contract { } /** - * Add a listener to receive all contract events emitted by the smart contract. - * The default is to listen for full contract events from the current block position. + * Add a listener to receive all contract 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. * @param {module:fabric-network.ContractListener} listener A contract listener callback function. * @param {module:fabric-network.ListenerOptions} [options] Listener options. * @returns {Promise} The added listener. * @example * const listener: ContractListener = async (event) => { - * if (event.eventName === 'newOrder') { + * if (event.eventName === 'newOrder') { * const details = event.payload.toString('utf8'); * // Run business process to handle orders * } diff --git a/fabric-network/src/events.ts b/fabric-network/src/events.ts index 0f9bdf5f93..07da0f6ea8 100644 --- a/fabric-network/src/events.ts +++ b/fabric-network/src/events.ts @@ -54,11 +54,13 @@ export interface ContractEvent { readonly type: EventType; readonly chaincodeId: string; readonly eventName: string; + readonly payload?: Buffer; getTransactionEvent(): TransactionEvent; } export interface FilteredContractEvent extends ContractEvent { readonly type: 'filtered'; + readonly payload: undefined; getTransactionEvent(): FilteredTransactionEvent; } diff --git a/fabric-network/src/impl/event/contractlistenersession.ts b/fabric-network/src/impl/event/contractlistenersession.ts index 5ca036324f..493319ca3c 100644 --- a/fabric-network/src/impl/event/contractlistenersession.ts +++ b/fabric-network/src/impl/event/contractlistenersession.ts @@ -34,20 +34,14 @@ export class ContractListenerSession implements ListenerSession { } private async onBlockEvent(blockEvent: BlockEvent): Promise { - for (const transactionEvent of blockEvent.getTransactionEvents()) { - await this.onTransactionEvent(transactionEvent); - } - } + const transactionPromises = blockEvent.getTransactionEvents() + .filter((transactionEvent) => transactionEvent.isValid) + .map((transactionEvent) => this.onTransactionEvent(transactionEvent)); - private async onTransactionEvent(transactionEvent: TransactionEvent): Promise { - if (transactionEvent.isValid) { - await this.onValidTransactionEvent(transactionEvent); - } else { - logger.debug('Ignored contract events for invalid transaction:', transactionEvent); - } + await Promise.all(transactionPromises); } - private async onValidTransactionEvent(transactionEvent: TransactionEvent): Promise { + private async onTransactionEvent(transactionEvent: TransactionEvent): Promise { for (const contractEvent of transactionEvent.getContractEvents()) { if (this.isMatch(contractEvent)) { await this.notifyListener(contractEvent); diff --git a/fabric-network/src/impl/event/filteredeventfactory.ts b/fabric-network/src/impl/event/filteredeventfactory.ts index 5d611c4eb4..2f31d2f242 100644 --- a/fabric-network/src/impl/event/filteredeventfactory.ts +++ b/fabric-network/src/impl/event/filteredeventfactory.ts @@ -69,6 +69,7 @@ function newFilteredContractEvent(transactionEvent: FilteredTransactionEvent, ch type: 'filtered', chaincodeId: chaincodeEvent.chaincode_id, eventName: chaincodeEvent.event_name, + payload: undefined, getTransactionEvent: () => transactionEvent }; return Object.freeze(contractEvent); diff --git a/fabric-network/test/impl/event/contractlistener.ts b/fabric-network/test/impl/event/contractlistener.ts index 0f8312d77e..5258f5636c 100644 --- a/fabric-network/test/impl/event/contractlistener.ts +++ b/fabric-network/test/impl/event/contractlistener.ts @@ -31,8 +31,9 @@ describe('contract event listener', () => { let listener: StubContractListener; let spyListener: sinon.SinonSpy<[ContractEvent], Promise>; let contract: Contract; - const eventName: string = 'eventName'; - const chaincodeId: string = 'bourbons'; + const eventName = 'eventName'; + const chaincodeId = 'bourbons'; + const eventPayload = 'payload'; beforeEach(async () => { eventService = new StubEventService('stub'); @@ -149,6 +150,7 @@ describe('contract event listener', () => { const chaincodeEvent = new protos.protos.ChaincodeEvent(); chaincodeEvent.chaincode_id = ccId; chaincodeEvent.event_name = eventName; + chaincodeEvent.payload = Buffer.from(eventPayload, 'utf8'); return chaincodeEvent; } @@ -369,4 +371,74 @@ describe('contract event listener', () => { const contractEvents = await listener.completePromise; expect(contractEvents[0].getTransactionEvent()).to.include({ isValid: true }); // tslint:disable-line: no-unused-expression }); + + it('filtered events do not contain payload', async () => { + const event = newFilteredEvent(1); + addFilteredTransaction(event, newFilteredTransaction()); + + const options: ListenerOptions = { + type: 'filtered' + }; + await contract.addContractListener(listener, options); + eventService.sendEvent(event); + const contractEvents = await listener.completePromise; + + expect(contractEvents[0].payload).to.be.undefined; // tslint:disable-line: no-unused-expression + }); + + it('full events contain payload', async () => { + const event = newEvent(1); + addTransaction(event, newTransaction()); + + const options: ListenerOptions = { + type: 'full' + }; + await contract.addContractListener(listener, options); + eventService.sendEvent(event); + const contractEvents = await listener.completePromise; + + expect(contractEvents[0].payload?.toString()).to.equal(eventPayload); + }); + + it('can navigate event heirarchy for filtered events', async () => { + const event = newFilteredEvent(1); + addFilteredTransaction(event, newFilteredTransaction()); + + const options: ListenerOptions = { + type: 'filtered' + }; + await contract.addContractListener(listener, options); + eventService.sendEvent(event); + const contractEvents = await listener.completePromise; + + const contractEvent = contractEvents[0]; + const transactionEvent = contractEvent.getTransactionEvent(); + expect(transactionEvent).to.exist; // tslint:disable-line: no-unused-expression + expect(transactionEvent.getContractEvents()).to.contain(contractEvent); + + const blockEvent = transactionEvent.getBlockEvent(); + expect(blockEvent).to.exist; // tslint:disable-line: no-unused-expression + expect(blockEvent.getTransactionEvents()).to.contain(transactionEvent); + }); + + it('can navigate event heirarchy for full events', async () => { + const event = newEvent(1); + addTransaction(event, newTransaction()); + + const options: ListenerOptions = { + type: 'full' + }; + await contract.addContractListener(listener, options); + eventService.sendEvent(event); + const contractEvents = await listener.completePromise; + + const contractEvent = contractEvents[0]; + const transactionEvent = contractEvent.getTransactionEvent(); + expect(transactionEvent).to.exist; // tslint:disable-line: no-unused-expression + expect(transactionEvent.getContractEvents()).to.contain(contractEvent); + + const blockEvent = transactionEvent.getBlockEvent(); + expect(blockEvent).to.exist; // tslint:disable-line: no-unused-expression + expect(blockEvent.getTransactionEvents()).to.contain(transactionEvent); + }); });