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 }