github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/kat/src/clients/event-streams.ts (about)

     1  // Copyright © 2021 Kaleido, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  import WebSocket from 'ws';
    16  import { config } from '../lib/config';
    17  import * as utils from '../lib/utils';
    18  import { IDBBlockchainData, IEventAssetDefinitionCreated, IEventAssetInstanceBatchCreated, IEventAssetInstanceCreated, IEventAssetInstancePropertySet, IEventPaymentDefinitionCreated, IEventPaymentInstanceCreated, IEventStreamMessage, IEventStreamRawMessageCorda } from '../lib/interfaces';
    19  import * as membersHandler from '../handlers/members';
    20  import * as assetDefinitionsHandler from '../handlers/asset-definitions';
    21  import * as paymentDefinitionsHandler from '../handlers/payment-definitions';
    22  import * as assetInstancesHandler from '../handlers/asset-instances';
    23  import * as paymentInstanceHandler from '../handlers/payment-instances';
    24  import { IEventMemberRegistered } from '../lib/interfaces';
    25  
    26  const log = utils.getLogger('clients/event-streams.ts');
    27  
    28  let ws: WebSocket;
    29  let heartBeatTimeout: NodeJS.Timeout;
    30  let disconnectionDetected = false;
    31  let disconnectionTimeout: NodeJS.Timeout;
    32  
    33  export const init = () => {
    34    ws = new WebSocket(config.eventStreams.wsEndpoint, {
    35      headers: {
    36        Authorization: 'Basic ' + Buffer.from(`${config.eventStreams.auth?.user ?? config.appCredentials.user}` +
    37          `:${config.eventStreams.auth?.password ?? config.appCredentials.password}`).toString('base64')
    38  
    39      }
    40    });
    41    addEventHandlers();
    42  };
    43  
    44  export const shutDown = () => {
    45    if (disconnectionTimeout) {
    46      clearTimeout(disconnectionTimeout);
    47    }
    48    if (ws) {
    49      clearTimeout(heartBeatTimeout);
    50      ws.close();
    51    }
    52  };
    53  
    54  const addEventHandlers = () => {
    55    ws.on('open', () => {
    56      if (disconnectionDetected) {
    57        disconnectionDetected = false;
    58        log.info('Event stream websocket restored');
    59      }
    60      ws.send(JSON.stringify({
    61        type: 'listen',
    62        topic: config.eventStreams.topic
    63      }));
    64      heartBeat();
    65    }).on('close', () => {
    66      disconnectionDetected = true;
    67      log.error(`Event stream websocket disconnected, attempting to reconnect in ${utils.constants.EVENT_STREAM_WEBSOCKET_RECONNECTION_DELAY_SECONDS} second(s)`);
    68      disconnectionTimeout = setTimeout(() => {
    69        init();
    70      }, utils.constants.EVENT_STREAM_WEBSOCKET_RECONNECTION_DELAY_SECONDS * 1000);
    71    }).on('message', async (message: string) => {
    72      await handleMessage(message);
    73      ws.send(JSON.stringify({
    74        type: 'ack',
    75        topic: config.eventStreams.topic
    76      }));
    77    }).on('pong', () => {
    78      heartBeat();
    79    }).on('error', err => {
    80      log.error(`Event stream websocket error. ${err}`);
    81    });
    82  };
    83  
    84  const heartBeat = () => {
    85    ws.ping();
    86    clearTimeout(heartBeatTimeout);
    87    heartBeatTimeout = setTimeout(() => {
    88      log.error('Event stream ping timeout');
    89      ws.terminate();
    90    }, utils.constants.EVENT_STREAM_PING_TIMEOUT_SECONDS * 1000);
    91  }
    92  
    93  const processRawMessage = (message: string): Array<IEventStreamMessage> => {
    94    switch (config.protocol) {
    95      case 'ethereum':
    96        return JSON.parse(message);
    97      case 'corda':
    98        const cordaMessages: Array<IEventStreamRawMessageCorda> = JSON.parse(message);
    99        return cordaMessages.map(msg => (
   100          {
   101            data: {
   102              ...msg.data.data,
   103              timestamp: Date.parse(msg.recordedTime)
   104            },
   105            transactionHash: msg.stateRef.txhash,
   106            subId: msg.subId,
   107            signature: msg.signature
   108          }
   109        )
   110        );
   111    }
   112  }
   113  
   114  const getBlockchainData = (message: IEventStreamMessage): IDBBlockchainData => {
   115    switch (config.protocol) {
   116      case 'ethereum':
   117        return {
   118          blockNumber: Number(message.blockNumber),
   119          transactionHash: message.transactionHash
   120        }
   121      case 'corda': {
   122        return {
   123          transactionHash: message.transactionHash
   124        }
   125      }
   126    }
   127  }
   128  
   129  const eventSignatures = () => {
   130    switch (config.protocol) {
   131      case 'ethereum': return utils.contractEventSignatures
   132      case 'corda': return utils.contractEventSignaturesCorda
   133    }
   134  }
   135  
   136  const handleMessage = async (message: string) => {
   137    const messageArray: Array<IEventStreamMessage> = processRawMessage(message);
   138    log.info(`Event batch (${messageArray.length})`)
   139    const signatures = eventSignatures();
   140    for (const message of messageArray) {
   141      log.trace(`Event ${JSON.stringify(message)}`);
   142      log.info(`Event signature: ${message.signature}`);
   143      const blockchainData: IDBBlockchainData = getBlockchainData(message);
   144      try {
   145        switch (message.signature) {
   146          case signatures.MEMBER_REGISTERED:
   147            await membersHandler.handleMemberRegisteredEvent(message.data as IEventMemberRegistered, blockchainData); break;
   148          case signatures.ASSET_DEFINITION_CREATED:
   149            await assetDefinitionsHandler.handleAssetDefinitionCreatedEvent(message.data as IEventAssetDefinitionCreated, blockchainData); break;
   150          case signatures.DESCRIBED_PAYMENT_DEFINITION_CREATED:
   151          case signatures.PAYMENT_DEFINITION_CREATED:
   152            await paymentDefinitionsHandler.handlePaymentDefinitionCreatedEvent(message.data as IEventPaymentDefinitionCreated, blockchainData); break;
   153          case signatures.ASSET_INSTANCE_CREATED:
   154          case signatures.DESCRIBED_ASSET_INSTANCE_CREATED:
   155            await assetInstancesHandler.handleAssetInstanceCreatedEvent(message.data as IEventAssetInstanceCreated, blockchainData); break;
   156          case signatures.ASSET_INSTANCE_BATCH_CREATED:
   157            await assetInstancesHandler.handleAssetInstanceBatchCreatedEvent(message.data as IEventAssetInstanceBatchCreated, blockchainData); break;
   158          case signatures.DESCRIBED_PAYMENT_INSTANCE_CREATED:
   159          case signatures.PAYMENT_INSTANCE_CREATED:
   160            await paymentInstanceHandler.handlePaymentInstanceCreatedEvent(message.data as IEventPaymentInstanceCreated, blockchainData); break;
   161          case signatures.ASSET_PROPERTY_SET:
   162            await assetInstancesHandler.handleSetAssetInstancePropertyEvent(message.data as IEventAssetInstancePropertySet, blockchainData); break;
   163        }
   164      } catch (err) {
   165        log.error(`Failed to handle event: ${message.signature} for message: ${JSON.stringify(message)} with error`, err.stack);
   166      }
   167    }
   168  };