github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/js/src/events.ts (about)

     1  import * as grpc from '@grpc/grpc-js';
     2  import { Event as BurrowEvent } from '../proto/exec_pb';
     3  import { IExecutionEventsClient } from '../proto/rpcevents_grpc_pb';
     4  import { BlockRange, BlocksRequest, Bound, EventsResponse } from '../proto/rpcevents_pb';
     5  import { Address } from './contracts/abi';
     6  import { Provider } from './solts/interface.gd';
     7  import BoundType = Bound.BoundType;
     8  
     9  export type EventStream = grpc.ClientReadableStream<EventsResponse>;
    10  
    11  export type Event = {
    12    header: Header;
    13    log: Log;
    14  };
    15  
    16  export type Header = {
    17    height: number;
    18    index: number;
    19    txHash: string;
    20    eventId: string;
    21  };
    22  
    23  export type Log = {
    24    data: Uint8Array;
    25    topics: Uint8Array[];
    26  };
    27  
    28  // We opt for a duck-type brand rather than a unique symbol so that dependencies using different versions of Burrow
    29  const SignalCodes = {
    30    cancelStream: 'cancelStream',
    31    endOfStream: 'endOfStream',
    32  } as const;
    33  
    34  type SignalCodes = keyof typeof SignalCodes;
    35  
    36  // can be used together when compatible (since Signal is exported by the Provider interface)
    37  export interface Signal<T extends SignalCodes> {
    38    __isBurrowSignal__: '__isBurrowSignal__';
    39    signal: T;
    40  }
    41  
    42  export type CancelStreamSignal = Signal<'cancelStream'>;
    43  
    44  const cancelStream: CancelStreamSignal = {
    45    __isBurrowSignal__: '__isBurrowSignal__',
    46    signal: 'cancelStream',
    47  } as const;
    48  
    49  export const Signal = Object.freeze({
    50    cancelStream,
    51  } as const);
    52  
    53  // Surprisingly, this seems to be as good as it gets at the time of writing (https://github.com/Microsoft/TypeScript/pull/17819)
    54  // that is, defining various types of union here does not help on the consumer side to infer exactly one of err or log
    55  // will be defined
    56  export type EventCallback<T> = (err?: Error, event?: T) => CancelStreamSignal | void;
    57  
    58  export type Bounds = number | 'first' | 'latest' | 'stream';
    59  
    60  export type FiniteBounds = number | 'first' | 'latest';
    61  
    62  type EventRegistry<T extends string> = Record<T, { signature: string }>;
    63  
    64  // Note: typescript breaks instanceof for Error (https://github.com/microsoft/TypeScript/issues/13965)
    65  const burrowSignalToken = '__isBurrowSignal__' as const;
    66  
    67  class EndOfStreamError extends Error implements Signal<'endOfStream'> {
    68    __isBurrowSignal__ = burrowSignalToken;
    69  
    70    public readonly signal = 'endOfStream';
    71  
    72    constructor() {
    73      super('End of stream, no more data will be sent - use isEndOfStream(err) to check for this signal');
    74    }
    75  }
    76  
    77  const endOfStreamError = Object.freeze(new EndOfStreamError());
    78  
    79  export function isBurrowSignal(value: unknown): value is Signal<SignalCodes> {
    80    const v = value as Signal<SignalCodes>;
    81    return v && v.__isBurrowSignal__ === burrowSignalToken && (v.signal === 'cancelStream' || v.signal === 'endOfStream');
    82  }
    83  
    84  export function isEndOfStream(value: unknown): value is Signal<'endOfStream'> {
    85    return isBurrowSignal(value) && value.signal === 'endOfStream';
    86  }
    87  
    88  export function isCancelStream(value: unknown): value is CancelStreamSignal {
    89    return isBurrowSignal(value) && value.signal === 'cancelStream';
    90  }
    91  
    92  export function getBlockRange(start: Bounds = 'latest', end: Bounds = 'stream'): BlockRange {
    93    const range = new BlockRange();
    94    range.setStart(boundsToBound(start));
    95    range.setEnd(boundsToBound(end));
    96    return range;
    97  }
    98  
    99  export function stream(
   100    client: IExecutionEventsClient,
   101    range: BlockRange,
   102    query: string,
   103    callback: EventCallback<Event>,
   104  ): EventStream {
   105    const arg = new BlocksRequest();
   106    arg.setBlockrange(range);
   107    arg.setQuery(query);
   108  
   109    const stream = client.events(arg);
   110    stream.on('data', (data: EventsResponse) => {
   111      const cancel = data
   112        .getEventsList()
   113        .map((event) => {
   114          try {
   115            return callback(undefined, burrowEventToInterfaceEvent(event));
   116          } catch (err) {
   117            stream.cancel();
   118            throw err;
   119          }
   120        })
   121        .find(isCancelStream);
   122      if (cancel) {
   123        stream.cancel();
   124      }
   125    });
   126    stream.on('end', () => callback(endOfStreamError));
   127    stream.on('error', (err: grpc.ServiceError) => err.code === grpc.status.CANCELLED || callback(err));
   128    return stream;
   129  }
   130  
   131  export type QueryOptions = {
   132    signatures: string[];
   133    address?: string;
   134  };
   135  
   136  export function queryFor({ signatures, address }: QueryOptions): string {
   137    return and(
   138      equals('EventType', 'LogEvent'),
   139      equals('Address', address),
   140      or(...signatures.map((s) => equals('Log0', s))),
   141    );
   142  }
   143  
   144  function and(...predicates: string[]): string {
   145    return predicates.filter((p) => p).join(' AND ');
   146  }
   147  
   148  function or(...predicates: string[]): string {
   149    const query = predicates.filter((p) => p).join(' OR ');
   150    if (!query) {
   151      return '';
   152    }
   153    return '(' + query + ')';
   154  }
   155  
   156  function equals(key: string, value?: string): string {
   157    if (!value) {
   158      return '';
   159    }
   160    return key + " = '" + value + "'";
   161  }
   162  
   163  function boundsToBound(bounds: Bounds): Bound {
   164    const bound = new Bound();
   165    bound.setIndex(0);
   166  
   167    switch (bounds) {
   168      case 'first':
   169        bound.setType(BoundType.FIRST);
   170        break;
   171      case 'latest':
   172        bound.setType(BoundType.LATEST);
   173        break;
   174      case 'stream':
   175        bound.setType(BoundType.STREAM);
   176        break;
   177      default:
   178        bound.setType(BoundType.ABSOLUTE);
   179        bound.setIndex(bounds);
   180    }
   181  
   182    return bound;
   183  }
   184  
   185  export function readEvents<T>(
   186    listener: (callback: EventCallback<T>, start?: Bounds, end?: Bounds) => unknown,
   187    start: FiniteBounds = 'first',
   188    end: FiniteBounds = 'latest',
   189    limit?: number,
   190  ): Promise<T[]> {
   191    return reduceEvents(
   192      listener,
   193      (events, event) => {
   194        if (limit && events.length === limit) {
   195          return Signal.cancelStream;
   196        }
   197        events.push(event);
   198        return events;
   199      },
   200      [] as T[],
   201      start,
   202      end,
   203    );
   204  }
   205  
   206  export function iterateEvents<T>(
   207    listener: (callback: EventCallback<T>, start?: Bounds, end?: Bounds) => unknown,
   208    reducer: (event: T) => CancelStreamSignal | void,
   209    start: FiniteBounds = 'first',
   210    end: FiniteBounds = 'latest',
   211  ): Promise<void> {
   212    return reduceEvents(listener, (acc, event) => reducer(event), undefined as void, start, end);
   213  }
   214  
   215  export function reduceEvents<T, U>(
   216    listener: (callback: EventCallback<T>, start?: Bounds, end?: Bounds) => unknown,
   217    reducer: (accumulator: U, event: T) => U | CancelStreamSignal,
   218    initialValue: U,
   219    start: FiniteBounds = 'first',
   220    end: FiniteBounds = 'latest',
   221  ): Promise<U> {
   222    let accumulator = initialValue;
   223    return new Promise<U>((resolve, reject) =>
   224      listener(
   225        (err, event) => {
   226          if (err) {
   227            if (isEndOfStream(err)) {
   228              return resolve(accumulator);
   229            }
   230            return reject(err);
   231          }
   232          if (!event) {
   233            reject(new Error(`received empty event`));
   234            return Signal.cancelStream;
   235          }
   236          const reduced = reducer(accumulator, event);
   237          if (isCancelStream(reduced)) {
   238            resolve(accumulator);
   239            return Signal.cancelStream;
   240          }
   241          accumulator = reduced;
   242        },
   243        start,
   244        end,
   245      ),
   246    );
   247  }
   248  
   249  export const listenerFor = <T extends string>(
   250    client: Provider,
   251    address: Address,
   252    eventRegistry: EventRegistry<T>,
   253    decode: (client: Provider, data?: Uint8Array, topics?: Uint8Array[]) => Record<T, () => unknown>,
   254    eventNames: T[],
   255  ) => (callback: EventCallback<{ name: T; payload: unknown; event: Event }>, start?: Bounds, end?: Bounds): unknown => {
   256    const signatureToName = eventNames.reduce((acc, n) => acc.set(eventRegistry[n].signature, n), new Map<string, T>());
   257  
   258    return client.listen(
   259      Array.from(signatureToName.keys()),
   260      address,
   261      (err, event) => {
   262        if (err) {
   263          return callback(err);
   264        }
   265        if (!event) {
   266          return callback(new Error(`Empty event received`));
   267        }
   268        const log0 = event.log.topics[0];
   269        if (!log0) {
   270          return callback(new Error(`Event has no Log0: ${event?.toString()}`));
   271        }
   272        const signature = Buffer.from(log0).toString('hex').toUpperCase();
   273        const name = signatureToName.get(signature);
   274        if (!name) {
   275          return callback(
   276            new Error(`Could not find event with signature ${signature} in registry: ${JSON.stringify(eventRegistry)}`),
   277          );
   278        }
   279        const payload = decode(client, event.log.data, event.log.topics)[name]();
   280        return callback(undefined, { name, payload, event });
   281      },
   282      start,
   283      end,
   284    );
   285  };
   286  
   287  export function burrowEventToInterfaceEvent(event: BurrowEvent): Event {
   288    const log = event.getLog();
   289    const header = event.getHeader();
   290    return {
   291      log: {
   292        data: log?.getData_asU8() ?? new Uint8Array(),
   293        topics: log?.getTopicsList_asU8() || [],
   294      },
   295      header: {
   296        height: header?.getHeight() ?? 0,
   297        index: header?.getIndex() ?? 0,
   298        eventId: header?.getEventid() ?? '',
   299  
   300        txHash: Buffer.from(header?.getTxhash_asU8() ?? []).toString('hex'),
   301      },
   302    };
   303  }