import { Observable, combineLatest, Subject, timer } from 'rxjs'; import { map, filter, startWith, tap, takeUntil, debounceTime } from 'rxjs/operators'; import { IValidationRequest } from '../interfaces/validation-request.i'; import { IValidationResponse } from '../interfaces/validation-response.i'; import { TransferRequest } from '../transfer-request'; import { WatcherProxy, WebsocketPayload } from '../interfaces/watcher-proxy.i'; import { ISingleValidationRequest } from '../interfaces/single-validation-request.i'; import { AddressValidation } from '../address-validation'; import { convertToAmount, convertAmount } from '../utils'; import { finalize } from 'rxjs/operators'; /** * @class Validator * @description This class encapsulate all the business logic for tracking transaction confirmation on different network * It manages watcher connection for the client and also format and filter watcher event so the client get only the information that is important to it */ export class Validator { constructor( private _watcherProxy: WatcherProxy, ) { } private uniformize({ network, addresses, nbConf, ...rest }: WebsocketPayload): WebsocketPayload { return { network: network.toUpperCase(), addresses: addresses.map(({ address, amount, onlyReqConf, requiredConf }) => { return { amount: Math.abs(Number(amount)), address, onlyReqConf, requiredConf: Number(requiredConf), } }), nbConf: Number(nbConf), ...rest, } } private accountSpecifcGuards({ from }: { from?: string }) { return filter((response: WebsocketPayload) => !from || response.addressFrom === from); } private getAddress(addresses: WebsocketPayload['addresses']) { return addresses[0].address; } /** * * @param response * @description Get a key from transfer request payload */ private getKeyFromRequest(request: IValidationRequest, prop: 'source' | 'destination') { console.log(`${prop}, Request: ${request[prop].address.toUpperCase()}:${request[prop].network.toUpperCase()}:${convertAmount(request.amount, request.ticker)}`); return `${request[prop].address.toUpperCase()}:${request[prop].network.toUpperCase()}:${convertAmount(request.amount, request.ticker)}`; } /** * * @param response * @description Get a key from single address validation request */ private getKeyFromSingleRequest(request: ISingleValidationRequest) { console.log(`${request.address.toUpperCase()}:${request.network.toUpperCase()}:${convertAmount(request.amount, request.ticker)}`); return `${request.address.toUpperCase()}:${request.network.toUpperCase()}:${convertAmount(request.amount, request.ticker)}`; } /** * * @param response * @description Get a key from watcher response */ private getKeyFromResponse(response: WebsocketPayload) { const address = this.getAddress(response.addresses); console.log(`Response: ${address.toUpperCase()}:${response.network.toUpperCase()}:${response.addresses[0].amount}`); return `${address.toUpperCase()}:${response.network.toUpperCase()}:${response.addresses[0].amount}` } /** * @description Stream operator to match watcher response with resquest */ private isResponseEventMatchingRequest = (request: IValidationRequest, prop: 'source' | 'destination') => filter((payload: WebsocketPayload) => this.getKeyFromResponse(payload) === this.getKeyFromRequest(request, prop)) /** * @description Stream operator to match watcher response with resquest */ private isResponseEventMatchingSingleRequest = (request: ISingleValidationRequest) => filter((payload: WebsocketPayload) => this.getKeyFromResponse(payload) === this.getKeyFromSingleRequest(request)); /** * * @param request Description of a transaction to watch (Require the network, amount, address and from in case of account) * @param timeout Time (miliseconds) to wait before a request complete. Default 10 minutes * @returns {Observable} Returns a stream of AddressValidation which indicate if a payment is confirmed on the * request network * * @description This functions encapsulate the core logic related to watching a transaction on a blockchain. It returns a stream of event * describing the state of the transaction on the network. It is also responsible to manage the connection with the augmented node, meaning that * it establish the connection with the augmented node, will reconnect in case of premature deconnection and will properly unsubscribe to watcher * in case of error, timeout or when the stream is completed (The transaction is confirmed) */ public validateSingleTransaction(request: ISingleValidationRequest, timeout = 60 * 1000 * 10): Observable { const takeUntilSubject = new Subject(); const timeoutSub = timer(timeout).subscribe(() => takeUntilSubject.next()); return this._watcherProxy.watch(request.id, request.address, request.network, request.nbConf).pipe( map(this.uniformize), // Filter out any event that does not match the request (based on the amount) this.isResponseEventMatchingSingleRequest(request), // Filter out any event that does not match the request (based on the from field) this.accountSpecifcGuards(request), ).pipe( map((validation) => { return { id: request.id, ...validation, requiredNbConf: request.nbConf, currentNbConf: validation.nbConf, amount: Number(convertToAmount(validation.addresses[0].amount, validation.network)) } }), tap(() => timeoutSub.unsubscribe()), // Map the event to an address validation a class that augment the event data with properties like isValid and isCompleted map((response) => new AddressValidation(response)), // This logic is responsible to handle the deconnexion to watcher in case of timeout, error or completed request // We debounce to be sure that the last event bubbled up into the stream before we complete the observable takeUntil(takeUntilSubject.pipe(debounceTime(1000))), tap((tr: AddressValidation) => { // If transaction is done we stop listening if (tr.isCompleted) { takeUntilSubject.next(); } }), // Unsubscribe in case of timeout, error or completed transfer request finalize(() => { this._watcherProxy.unwatch(request.id, request.address, request.network, request.nbConf); timeoutSub.unsubscribe(); }) ); }; /** * * @param request Description of a transfer request to watch (Require the network, amount, ticker, address and from in case of account for both source and destination ) * @param timeout Time (miliseconds) to wait before a request complete. Default 10 minutes * @returns {Observable} Returns a stream of TransferRequest which indicate if a transfer request is confirmed on the * source and destination network * * @description This functions encapsulate the core logic related to watching a transfer request. It returns a stream of event * describing the state of the transfer request on the both networks. It is also responsible to manage the connection with the augmented node, meaning that * it establish the connection with the augmented node, will reconnect in case of premature deconnection and will properly unsubcsribe to watcher * in case of error, timeout or when the stream is completed (The transfer request is completed) */ public validate(request: IValidationRequest, timeout = 60 * 1000 * 10): Observable { const takeUntilSubject = new Subject(); const timeoutSub = timer(timeout).subscribe(() => takeUntilSubject.next()); return combineLatest( this._watcherProxy.watch(request.id, request.source.address, request.source.network, request.source.nbConf, request.from, convertAmount(request.amount, request.ticker) as any).pipe( map(this.uniformize), // Filter out any event that does not match the request (based on the amount) this.isResponseEventMatchingRequest(request, 'source'), // Filter out any event that does not match the request (based on the from field) this.accountSpecifcGuards(request), startWith(null), ), this._watcherProxy.watch(request.id, request.destination.address, request.destination.network, request.destination.nbConf, request.from, convertAmount(request.amount, request.ticker) as any).pipe( map(this.uniformize), // Filter out any event that does not match the request (based on the amount) this.isResponseEventMatchingRequest(request, 'destination'), // Filter out any event that does not match the request (based on the from field) this.accountSpecifcGuards(request), startWith(null), ) ).pipe( tap(([source, destination]) => { if ((source || destination) && !timeoutSub.closed) { timeoutSub.unsubscribe() } }), map(([source, destination]): IValidationResponse => { const src = source as WebsocketPayload; const dest = destination as WebsocketPayload; let sourcePayload = null; let destinationPayload = null; if (source) { sourcePayload = { ...src, amount: Number(convertToAmount(src.addresses[0].amount, src.network)), txHash: src.txHash, currentNbConf: src.nbConf, requiredNbConf: request.source.nbConf, } } if (destination) { destinationPayload = { ...dest, amount: Number(convertToAmount(dest.addresses[0].amount, src.network)), currentNbConf: dest.nbConf, txHash: dest.txHash, requiredNbConf: request.destination.nbConf } } return { id: request.id, ticker: request.ticker, source: sourcePayload, dest: destinationPayload } }), // Map the event to an address validation a class that augment the event data with properties like isValid and isCompleted // This logic is responsible to handle the deconnexion to watcher in case of timeout, error or completed request map(response => new TransferRequest(response)), // We throttle to be sure that the last event bubbled up into the stream before we complete the observable takeUntil(takeUntilSubject.pipe(debounceTime(1000))), tap((tr: TransferRequest) => { // If transfer request is done we stop listening if (tr.isCompleted) { takeUntilSubject.next(); } }), // Unsubscribe in case of timeout, error or completed transfer request finalize(() => { this._watcherProxy.unwatch(request.id, request.source.address, request.source.network, request.source.nbConf); this._watcherProxy.unwatch(request.id, request.destination.address, request.destination.network, request.destination.nbConf); timeoutSub.unsubscribe(); }) ) } }