import { Injectable, OnDestroy } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Subject, of } from 'rxjs'; import { map, mergeMap, catchError, takeUntil } from 'rxjs/operators'; import { Location } from '@angular/common'; import { environment, API_URL } from '../../../environments/environment'; import { LoggerService } from '../../shared/services/logger.service'; import { CommonService } from '../../shared/services/common.service'; import { SessionService } from '../../shared/services/session.service'; import { WebSocketClientService } from '../../shared/services/web-socket.service'; import { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.component'; import { CLNInvoiceInformationComponent } from '../transactions/invoices/invoice-information-modal/invoice-information.component'; import { GetInfo, Fees, Balance, LocalRemoteBalance, Payment, FeeRates, ListInvoices, Invoice, Peer, OnChain, QueryRoutes, SaveChannel, GetNewAddress, DetachPeer, UpdateChannel, CloseChannel, SendPayment, GetQueryRoutes, ChannelLookup, FetchInvoices, Channel, OfferInvoice, Offer, ListForwards, FetchListForwards, ForwardingEvent, LocalFailedEvent } from '../../shared/models/clnModels'; import { AlertTypeEnum, APICallStatusEnum, UI_MESSAGES, CLNWSEventTypeEnum, CLNActions, RTLActions, CLNForwardingEventsStatusEnum } from '../../shared/services/consts-enums-functions'; import { closeAllDialogs, closeSpinner, logout, openAlert, openSnackBar, openSpinner, setApiUrl, setNodeData } from '../../store/rtl.actions'; import { RTLState } from '../../store/rtl.state'; import { addUpdateOfferBookmark, fetchBalance, fetchChannels, fetchFeeRates, fetchFees, fetchInvoices, fetchLocalRemoteBalance, fetchPayments, fetchPeers, fetchUTXOs, getForwardingHistory, setLookup, setPeers, setQueryRoutes, updateCLAPICallStatus, updateInvoice, setOfferInvoice, sendPaymentStatus, setForwardingHistory } from './cln.actions'; import { allAPIsCallStatus, clnNodeInformation } from './cln.selector'; import { ApiCallsListCL } from '../../shared/models/apiCallsPayload'; import { CLNOfferInformationComponent } from '../transactions/offers/offer-information-modal/offer-information.component'; @Injectable() export class CLNEffects implements OnDestroy { CHILD_API_URL = API_URL + '/cln'; private flgInitialized = false; private unSubs: Array> = [new Subject(), new Subject(), new Subject()]; constructor( private actions: Actions, private httpClient: HttpClient, private store: Store, private sessionService: SessionService, private commonService: CommonService, private logger: LoggerService, private router: Router, private wsService: WebSocketClientService, private location: Location ) { this.store.select(allAPIsCallStatus).pipe(takeUntil(this.unSubs[0])).subscribe((allApisCallStatus: ApiCallsListCL) => { if ( ((allApisCallStatus.FetchInfo.status === APICallStatusEnum.COMPLETED || allApisCallStatus.FetchInfo.status === APICallStatusEnum.ERROR) && (allApisCallStatus.FetchFees.status === APICallStatusEnum.COMPLETED || allApisCallStatus.FetchFees.status === APICallStatusEnum.ERROR) && (allApisCallStatus.FetchChannels.status === APICallStatusEnum.COMPLETED || allApisCallStatus.FetchChannels.status === APICallStatusEnum.ERROR) && (allApisCallStatus.FetchBalance.status === APICallStatusEnum.COMPLETED || allApisCallStatus.FetchBalance.status === APICallStatusEnum.ERROR) && (allApisCallStatus.FetchLocalRemoteBalance.status === APICallStatusEnum.COMPLETED || allApisCallStatus.FetchLocalRemoteBalance.status === APICallStatusEnum.ERROR)) && !this.flgInitialized ) { this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.INITALIZE_NODE_DATA })); this.flgInitialized = true; } }); this.wsService.clWSMessages.pipe( takeUntil(this.unSubs[1])). subscribe((newMessage) => { this.logger.info('Received new message from the service: ' + JSON.stringify(newMessage)); if (newMessage) { switch (newMessage.event) { case CLNWSEventTypeEnum.INVOICE: this.logger.info(newMessage); if (newMessage && newMessage.data && newMessage.data.label) { this.store.dispatch(updateInvoice({ payload: newMessage.data })); } break; case CLNWSEventTypeEnum.SEND_PAYMENT: this.logger.info(newMessage); break; case CLNWSEventTypeEnum.BLOCK_HEIGHT: this.logger.info(newMessage); break; default: this.logger.info('Received Event from WS: ' + JSON.stringify(newMessage)); break; } } }); } infoFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_INFO_CLN), mergeMap((action: { type: string, payload: { loadPage: string } }) => { this.flgInitialized = false; this.store.dispatch(setApiUrl({ payload: this.CHILD_API_URL })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchInfo', status: APICallStatusEnum.INITIATED } })); this.store.dispatch(openSpinner({ payload: UI_MESSAGES.GET_NODE_INFO })); return this.httpClient.get(this.CHILD_API_URL + environment.GETINFO_API). pipe( takeUntil(this.actions.pipe(ofType(RTLActions.SET_SELECTED_NODE))), map((info) => { this.logger.info(info); if (info.chains && info.chains.length && info.chains[0] && (typeof info.chains[0] === 'object' && info.chains[0].hasOwnProperty('chain') && info?.chains[0].chain && info?.chains[0].chain.toLowerCase().indexOf('bitcoin') < 0) ) { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchInfo', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.GET_NODE_INFO })); this.store.dispatch(closeAllDialogs()); setTimeout(() => { this.store.dispatch(openAlert({ payload: { data: { type: AlertTypeEnum.ERROR, alertTitle: 'Shitcoin Found', titleMessage: 'Sorry Not Sorry, RTL is Bitcoin Only!' } } })); }, 500); return { type: RTLActions.LOGOUT }; } else { this.initializeRemainingData(info, action.payload.loadPage); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchInfo', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.GET_NODE_INFO })); return { type: CLNActions.SET_INFO_CLN, payload: info ? info : {} }; } }), catchError((err) => { const code = this.commonService.extractErrorCode(err); const msg = (code === 'ETIMEDOUT') ? 'Unable to Connect to Core Lightning Server.' : this.commonService.extractErrorMessage(err); this.router.navigate(['/error'], { state: { errorCode: code, errorMessage: msg } }); this.handleErrorWithoutAlert('FetchInfo', UI_MESSAGES.GET_NODE_INFO, 'Fetching Node Info Failed.', { status: code, error: msg }); return of({ type: RTLActions.VOID }); }) ); }) )); fetchFeesCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_FEES_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchFees', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.FEES_API); }), map((fees) => { this.logger.info(fees); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchFees', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_FEES_CLN, payload: fees ? fees : {} }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchFees', UI_MESSAGES.NO_SPINNER, 'Fetching Fees Failed.', err); return of({ type: RTLActions.VOID }); }) )); fetchFeeRatesCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_FEE_RATES_CLN), mergeMap((action: { type: string, payload: string }) => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchFeeRates' + action.payload, status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.NETWORK_API + '/feeRates/' + action.payload). pipe( map((feeRates) => { this.logger.info(feeRates); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchFeeRates' + action.payload, status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_FEE_RATES_CLN, payload: feeRates ? feeRates : {} }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchFeeRates' + action.payload, UI_MESSAGES.NO_SPINNER, 'Fetching Fee Rates Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); fetchBalanceCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_BALANCE_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchBalance', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.BALANCE_API); }), map((balance) => { this.logger.info(balance); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchBalance', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_BALANCE_CLN, payload: balance ? balance : {} }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchBalance', UI_MESSAGES.NO_SPINNER, 'Fetching Balances Failed.', err); return of({ type: RTLActions.VOID }); }) )); fetchLocalRemoteBalanceCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_LOCAL_REMOTE_BALANCE_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchLocalRemoteBalance', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/localRemoteBalance'); }), map((lrBalance) => { this.logger.info(lrBalance); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchLocalRemoteBalance', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_LOCAL_REMOTE_BALANCE_CLN, payload: lrBalance ? lrBalance : {} }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchLocalRemoteBalance', UI_MESSAGES.NO_SPINNER, 'Fetching Balances Failed.', err); return of({ type: RTLActions.VOID }); }) )); getNewAddressCL = createEffect(() => this.actions.pipe( ofType(CLNActions.GET_NEW_ADDRESS_CLN), mergeMap((action: { type: string, payload: GetNewAddress }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.GENERATE_NEW_ADDRESS })); return this.httpClient.get(this.CHILD_API_URL + environment.ON_CHAIN_API + '?type=' + action.payload.addressCode). pipe( map((newAddress: any) => { this.logger.info(newAddress); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.GENERATE_NEW_ADDRESS })); return { type: CLNActions.SET_NEW_ADDRESS_CLN, payload: (newAddress && newAddress.address) ? newAddress.address : {} }; }), catchError((err: any) => { this.handleErrorWithAlert('GenerateNewAddress', UI_MESSAGES.GENERATE_NEW_ADDRESS, 'Generate New Address Failed', this.CHILD_API_URL + environment.ON_CHAIN_API + '?type=' + action.payload.addressId, err); return of({ type: RTLActions.VOID }); }) ); }) )); setNewAddressCL = createEffect( () => this.actions.pipe( ofType(CLNActions.SET_NEW_ADDRESS_CLN), map((action: { type: string, payload: string }) => { this.logger.info(action.payload); return action.payload; }) ), { dispatch: false } ); peersFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_PEERS_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchPeers', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.PEERS_API). pipe( map((peers: any) => { this.logger.info(peers); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchPeers', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_PEERS_CLN, payload: peers || [] }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchPeers', UI_MESSAGES.NO_SPINNER, 'Fetching Peers Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); saveNewPeerCL = createEffect(() => this.actions.pipe( ofType(CLNActions.SAVE_NEW_PEER_CLN), mergeMap((action: { type: string, payload: { id: string } }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.CONNECT_PEER })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewPeer', status: APICallStatusEnum.INITIATED } })); return this.httpClient.post(this.CHILD_API_URL + environment.PEERS_API, { id: action.payload.id }). pipe( map((postRes: Peer[]) => { this.logger.info(postRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewPeer', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.CONNECT_PEER })); this.store.dispatch(setPeers({ payload: (postRes || []) })); return { type: CLNActions.NEWLY_ADDED_PEER_CLN, payload: { peer: postRes.find((peer: Peer) => action.payload.id.indexOf(peer.id ? peer.id : '') === 0) } }; }), catchError((err: any) => { this.handleErrorWithoutAlert('SaveNewPeer', UI_MESSAGES.CONNECT_PEER, 'Peer Connection Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); detachPeerCL = createEffect(() => this.actions.pipe( ofType(CLNActions.DETACH_PEER_CLN), mergeMap((action: { type: string, payload: DetachPeer }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DISCONNECT_PEER })); return this.httpClient.delete(this.CHILD_API_URL + environment.PEERS_API + '/' + action.payload.id + '?force=' + action.payload.force). pipe( map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DISCONNECT_PEER })); this.store.dispatch(openSnackBar({ payload: 'Peer Disconnected Successfully!' })); return { type: CLNActions.REMOVE_PEER_CLN, payload: { id: action.payload.id } }; }), catchError((err: any) => { this.handleErrorWithAlert('PeerDisconnect', UI_MESSAGES.DISCONNECT_PEER, 'Unable to Detach Peer. Try again later.', this.CHILD_API_URL + environment.PEERS_API + '/' + action.payload.id, err); return of({ type: RTLActions.VOID }); }) ); }) )); channelsFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_CHANNELS_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchChannels', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/listChannels'); }), map((channels: Channel[]) => { this.logger.info(channels); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchChannels', status: APICallStatusEnum.COMPLETED } })); // this.store.dispatch(getForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.SETTLED } })); const sortedChannels = { activeChannels: [], pendingChannels: [], inactiveChannels: [] }; channels.forEach((channel) => { if (channel.state === 'CHANNELD_NORMAL') { if (channel.connected) { sortedChannels.activeChannels.push(channel); } else { sortedChannels.inactiveChannels.push(channel); } } else { sortedChannels.pendingChannels.push(channel); } }); return { type: CLNActions.SET_CHANNELS_CLN, payload: sortedChannels }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchChannels', UI_MESSAGES.NO_SPINNER, 'Fetching Channels Failed.', err); return of({ type: RTLActions.VOID }); }) )); openNewChannelCL = createEffect(() => this.actions.pipe( ofType(CLNActions.SAVE_NEW_CHANNEL_CLN), mergeMap((action: { type: string, payload: SaveChannel }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.OPEN_CHANNEL })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewChannel', status: APICallStatusEnum.INITIATED } })); const newPayload = { id: action.payload.peerId, satoshis: action.payload.satoshis, feeRate: action.payload.feeRate, announce: action.payload.announce }; if (action.payload.minconf) { newPayload['minconf'] = action.payload.minconf; } if (action.payload.utxos) { newPayload['utxos'] = action.payload.utxos; } if (action.payload.requestAmount) { newPayload['request_amt'] = action.payload.requestAmount; } if (action.payload.compactLease) { newPayload['compact_lease'] = action.payload.compactLease; } return this.httpClient.post(this.CHILD_API_URL + environment.CHANNELS_API, newPayload). pipe( map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewChannel', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.OPEN_CHANNEL })); this.store.dispatch(openSnackBar({ payload: 'Channel Added Successfully!' })); this.store.dispatch(fetchBalance()); this.store.dispatch(fetchUTXOs()); return { type: CLNActions.FETCH_CHANNELS_CLN }; }), catchError((err: any) => { this.handleErrorWithoutAlert('SaveNewChannel', UI_MESSAGES.OPEN_CHANNEL, 'Opening Channel Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); updateChannelCL = createEffect(() => this.actions.pipe( ofType(CLNActions.UPDATE_CHANNEL_CLN), mergeMap((action: { type: string, payload: UpdateChannel }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.UPDATE_CHAN_POLICY })); return this.httpClient.post( this.CHILD_API_URL + environment.CHANNELS_API + '/setChannelFee', { id: action.payload.channelId, base: action.payload.baseFeeMsat, ppm: action.payload.feeRate } ).pipe(map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.UPDATE_CHAN_POLICY })); if (action.payload.channelId === 'all') { this.store.dispatch(openSnackBar({ payload: { message: 'All Channels Updated Successfully. Fee policy updates may take some time to reflect on the channel.', duration: 5000 } })); } else { this.store.dispatch(openSnackBar({ payload: { message: 'Channel Updated Successfully. Fee policy updates may take some time to reflect on the channel.', duration: 5000 } })); } return { type: CLNActions.FETCH_CHANNELS_CLN }; }), catchError((err: any) => { this.handleErrorWithAlert('UpdateChannel', UI_MESSAGES.UPDATE_CHAN_POLICY, 'Update Channel Failed', this.CHILD_API_URL + environment.CHANNELS_API, err); return of({ type: RTLActions.VOID }); }) ); }) )); closeChannelCL = createEffect(() => this.actions.pipe( ofType(CLNActions.CLOSE_CHANNEL_CLN), mergeMap((action: { type: string, payload: CloseChannel }) => { this.store.dispatch(openSpinner({ payload: (action.payload.force ? UI_MESSAGES.FORCE_CLOSE_CHANNEL : UI_MESSAGES.CLOSE_CHANNEL) })); const queryParam = action.payload.force ? '?force=' + action.payload.force : ''; return this.httpClient.delete(this.CHILD_API_URL + environment.CHANNELS_API + '/' + action.payload.channelId + queryParam). pipe( map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(closeSpinner({ payload: action.payload.force ? UI_MESSAGES.FORCE_CLOSE_CHANNEL : UI_MESSAGES.CLOSE_CHANNEL })); this.store.dispatch(fetchChannels()); this.store.dispatch(fetchLocalRemoteBalance()); this.store.dispatch(openSnackBar({ payload: 'Channel Closed Successfully!' })); return { type: CLNActions.REMOVE_CHANNEL_CLN, payload: action.payload }; }), catchError((err: any) => { this.handleErrorWithAlert('CloseChannel', (action.payload.force ? UI_MESSAGES.FORCE_CLOSE_CHANNEL : UI_MESSAGES.CLOSE_CHANNEL), 'Unable to Close Channel. Try again later.', this.CHILD_API_URL + environment.CHANNELS_API, err); return of({ type: RTLActions.VOID }); }) ); }) )); paymentsFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_PAYMENTS_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchPayments', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.PAYMENTS_API); }), map((payments) => { this.logger.info(payments); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchPayments', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_PAYMENTS_CLN, payload: payments || [] }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchPayments', UI_MESSAGES.NO_SPINNER, 'Fetching Payments Failed.', err); return of({ type: RTLActions.VOID }); }) )); fetchOfferInvoiceCL = createEffect( () => this.actions.pipe( ofType(CLNActions.FETCH_OFFER_INVOICE_CLN), mergeMap((action: { type: string, payload: { offer: string, msatoshi?: string } }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.FETCH_INVOICE })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchOfferInvoice', status: APICallStatusEnum.INITIATED } })); return this.httpClient.post(this.CHILD_API_URL + environment.OFFERS_API + '/fetchOfferInvoice', action.payload). pipe( map((fetchedInvoice: any) => { this.logger.info(fetchedInvoice); setTimeout(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchOfferInvoice', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.FETCH_INVOICE })); this.store.dispatch(setOfferInvoice({ payload: (fetchedInvoice ? fetchedInvoice : {}) })); }, 500); }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchOfferInvoice', UI_MESSAGES.FETCH_INVOICE, 'Offer Invoice Fetch Failed', err); return of({ type: RTLActions.VOID }); })); })), { dispatch: false } ); setOfferInvoiceCL = createEffect( () => this.actions.pipe( ofType(CLNActions.SET_OFFER_INVOICE_CLN), map((action: { type: string, payload: OfferInvoice }) => { this.logger.info(action.payload); return action.payload; }) ), { dispatch: false } ); sendPaymentCL = createEffect( () => this.actions.pipe( ofType(CLNActions.SEND_PAYMENT_CLN), mergeMap((action: { type: string, payload: SendPayment }) => { this.store.dispatch(openSpinner({ payload: action.payload.uiMessage })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.INITIATED } })); return this.httpClient.post(this.CHILD_API_URL + environment.PAYMENTS_API, action.payload).pipe( map((sendRes: any) => { this.logger.info(sendRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.COMPLETED } })); let snackBarMessageStr = 'Payment Sent Successfully!'; if (sendRes.saveToDBError) { snackBarMessageStr = 'Payment Sent Successfully but Offer Saving to Database Failed.'; } if (sendRes.saveToDBResponse && sendRes.saveToDBResponse !== 'NA') { this.store.dispatch(addUpdateOfferBookmark({ payload: sendRes.saveToDBResponse })); snackBarMessageStr = 'Payment Sent Successfully and Offer Saved to Database.'; } setTimeout(() => { this.store.dispatch(fetchChannels()); this.store.dispatch(fetchBalance()); this.store.dispatch(fetchPayments()); this.store.dispatch(closeSpinner({ payload: action.payload.uiMessage })); this.store.dispatch(openSnackBar({ payload: snackBarMessageStr })); this.store.dispatch(sendPaymentStatus({ payload: sendRes.paymentResponse })); }, 1000); }), catchError((err: any) => { this.logger.error('Error: ' + JSON.stringify(err)); if (action.payload.fromDialog) { this.handleErrorWithoutAlert('SendPayment', action.payload.uiMessage, 'Send Payment Failed.', err); } else { this.handleErrorWithAlert('SendPayment', action.payload.uiMessage, 'Send Payment Failed', this.CHILD_API_URL + environment.PAYMENTS_API, err); } return of({ type: RTLActions.VOID }); }) ); })), { dispatch: false } ); queryRoutesFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.GET_QUERY_ROUTES_CLN), mergeMap((action: { type: string, payload: GetQueryRoutes }) => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'GetQueryRoutes', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.NETWORK_API + '/getRoute/' + action.payload.destPubkey + '/' + action.payload.amount). pipe( map((qrRes: any) => { this.logger.info(qrRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'GetQueryRoutes', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_QUERY_ROUTES_CLN, payload: qrRes }; }), catchError((err: any) => { this.store.dispatch(setQueryRoutes({ payload: { routes: [] } })); this.handleErrorWithAlert('GetQueryRoutes', UI_MESSAGES.NO_SPINNER, 'Get Query Routes Failed', this.CHILD_API_URL + environment.NETWORK_API + '/getRoute/' + action.payload.destPubkey + '/' + action.payload.amount, err); return of({ type: RTLActions.VOID }); }) ); }) )); setQueryRoutesCL = createEffect( () => this.actions.pipe( ofType(CLNActions.SET_QUERY_ROUTES_CLN), map((action: { type: string, payload: QueryRoutes }) => action.payload) ), { dispatch: false } ); peerLookupCL = createEffect(() => this.actions.pipe( ofType(CLNActions.PEER_LOOKUP_CLN), mergeMap((action: { type: string, payload: string }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.SEARCHING_NODE })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'Lookup', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.NETWORK_API + '/listNode/' + action.payload). pipe( map((resPeer) => { this.logger.info(resPeer); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'Lookup', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.SEARCHING_NODE })); return { type: CLNActions.SET_LOOKUP_CLN, payload: resPeer }; }), catchError((err: any) => { this.handleErrorWithAlert('Lookup', UI_MESSAGES.SEARCHING_NODE, 'Peer Lookup Failed', this.CHILD_API_URL + environment.NETWORK_API + '/listNode/' + action.payload, err); return of({ type: RTLActions.VOID }); }) ); }) )); channelLookupCL = createEffect(() => this.actions.pipe( ofType(CLNActions.CHANNEL_LOOKUP_CLN), mergeMap((action: { type: string, payload: ChannelLookup }) => { this.store.dispatch(openSpinner({ payload: action.payload.uiMessage })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'Lookup', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.NETWORK_API + '/listChannel/' + action.payload.shortChannelID). pipe( map((resChannel) => { this.logger.info(resChannel); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'Lookup', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: action.payload.uiMessage })); return { type: CLNActions.SET_LOOKUP_CLN, payload: resChannel }; }), catchError((err: any) => { if (action.payload.showError) { this.handleErrorWithAlert('Lookup', action.payload.uiMessage, 'Channel Lookup Failed', this.CHILD_API_URL + environment.NETWORK_API + '/listChannel/' + action.payload.shortChannelID, err); } else { this.store.dispatch(closeSpinner({ payload: action.payload.uiMessage })); } this.store.dispatch(setLookup({ payload: [] })); return of({ type: RTLActions.VOID }); }) ); }) )); invoiceLookupCL = createEffect(() => this.actions.pipe( ofType(CLNActions.INVOICE_LOOKUP_CLN), mergeMap((action: { type: string, payload: string }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.SEARCHING_INVOICE })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'Lookup', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.INVOICES_API + '?label=' + action.payload). pipe( map((resInvoice: any) => { this.logger.info(resInvoice); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'Lookup', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.SEARCHING_INVOICE })); if (resInvoice.invoices && resInvoice.invoices.length && resInvoice.invoices.length > 0) { this.store.dispatch(updateInvoice({ payload: resInvoice.invoices[0] })); } return { type: CLNActions.SET_LOOKUP_CLN, payload: resInvoice.invoices && resInvoice.invoices.length && resInvoice.invoices.length > 0 ? resInvoice.invoices[0] : resInvoice }; }), catchError((err: any) => { this.handleErrorWithoutAlert('Lookup', UI_MESSAGES.SEARCHING_INVOICE, 'Invoice Lookup Failed', err); this.store.dispatch(openSnackBar({ payload: { message: 'Invoice Refresh Failed.', type: 'ERROR' } })); return of({ type: RTLActions.VOID }); }) ); }) )); setLookupCL = createEffect( () => this.actions.pipe( ofType(CLNActions.SET_LOOKUP_CLN), map((action: { type: string, payload: any }) => { this.logger.info(action.payload); return action.payload; }) ), { dispatch: false } ); fetchForwardingHistoryCL = createEffect(() => this.actions.pipe( ofType(CLNActions.GET_FORWARDING_HISTORY_CLN), mergeMap((action: { type: string, payload: { status: string } }) => { const statusInitial = action.payload.status.charAt(0).toUpperCase(); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchForwardingHistory' + statusInitial, status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=' + action.payload.status). pipe( map((fhRes: any) => { this.logger.info(fhRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchForwardingHistory' + statusInitial, status: APICallStatusEnum.COMPLETED } })); if (action.payload.status === CLNForwardingEventsStatusEnum.FAILED) { this.store.dispatch(setForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.FAILED, totalForwards: fhRes.length, listForwards: fhRes } })); } else if (action.payload.status === CLNForwardingEventsStatusEnum.LOCAL_FAILED) { this.store.dispatch(setForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.LOCAL_FAILED, totalForwards: fhRes.length, listForwards: fhRes } })); } else if (action.payload.status === CLNForwardingEventsStatusEnum.SETTLED) { this.store.dispatch(setForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.SETTLED, totalForwards: fhRes.length, listForwards: fhRes } })); } return { type: RTLActions.VOID }; }), catchError((err: any) => { this.handleErrorWithAlert('FetchForwardingHistory' + statusInitial, UI_MESSAGES.NO_SPINNER, 'Get ' + action.payload.status + ' Forwarding History Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=' + action.payload.status, err); return of({ type: RTLActions.VOID }); }) ); }) )); deleteExpiredInvoiceCL = createEffect(() => this.actions.pipe( ofType(CLNActions.DELETE_EXPIRED_INVOICE_CLN), mergeMap((action: { type: string, payload: number }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DELETE_INVOICE })); const queryStr = (action.payload) ? '?maxexpiry=' + action.payload : ''; return this.httpClient.delete(this.CHILD_API_URL + environment.INVOICES_API + queryStr). pipe( map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DELETE_INVOICE })); this.store.dispatch(openSnackBar({ payload: 'Invoices Deleted Successfully!' })); return { type: CLNActions.FETCH_INVOICES_CLN, payload: { num_max_invoices: 1000000, reversed: true } }; }), catchError((err: any) => { this.handleErrorWithAlert('DeleteInvoices', UI_MESSAGES.DELETE_INVOICE, 'Delete Invoice Failed', this.CHILD_API_URL + environment.INVOICES_API, err); return of({ type: RTLActions.VOID }); }) ); }) )); saveNewInvoiceCL = createEffect(() => this.actions.pipe( ofType(CLNActions.SAVE_NEW_INVOICE_CLN), mergeMap((action: { type: string, payload: { amount: number, label: string, description: string, expiry: number, private: boolean } }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.ADD_INVOICE })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewInvoice', status: APICallStatusEnum.INITIATED } })); return this.httpClient.post(this.CHILD_API_URL + environment.INVOICES_API, { label: action.payload.label, amount: action.payload.amount, description: action.payload.description, expiry: action.payload.expiry, private: action.payload.private }). pipe( map((postRes: Invoice) => { this.logger.info(postRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewInvoice', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.ADD_INVOICE })); postRes.msatoshi = action.payload.amount; postRes.label = action.payload.label; postRes.expires_at = Math.round((new Date().getTime() / 1000) + action.payload.expiry); postRes.description = action.payload.description; postRes.status = 'unpaid'; setTimeout(() => { this.store.dispatch(openAlert({ payload: { data: { invoice: postRes, newlyAdded: true, component: CLNInvoiceInformationComponent } } })); }, 100); return { type: CLNActions.ADD_INVOICE_CLN, payload: postRes }; }), catchError((err: any) => { this.handleErrorWithoutAlert('SaveNewInvoice', UI_MESSAGES.ADD_INVOICE, 'Add Invoice Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); saveNewOfferCL = createEffect(() => this.actions.pipe( ofType(CLNActions.SAVE_NEW_OFFER_CLN), mergeMap((action: { type: string, payload: { amount: string, description: string, vendor: string } }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.CREATE_OFFER })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewOffer', status: APICallStatusEnum.INITIATED } })); return this.httpClient.post(this.CHILD_API_URL + environment.OFFERS_API, { amount: action.payload.amount, description: action.payload.description, vendor: action.payload.vendor }).pipe(map((postRes: Offer) => { this.logger.info(postRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewOffer', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.CREATE_OFFER })); setTimeout(() => { this.store.dispatch(openAlert({ payload: { data: { offer: postRes, newlyAdded: true, component: CLNOfferInformationComponent } } })); }, 100); return { type: CLNActions.ADD_OFFER_CLN, payload: postRes }; }), catchError((err: any) => { this.handleErrorWithoutAlert('SaveNewOffer', UI_MESSAGES.CREATE_OFFER, 'Create Offer Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); invoicesFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_INVOICES_CLN), mergeMap((action: { type: string, payload: FetchInvoices }) => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchInvoices', status: APICallStatusEnum.INITIATED } })); const num_max_invoices = (action.payload.num_max_invoices) ? action.payload.num_max_invoices : 1000000; const index_offset = (action.payload.index_offset) ? action.payload.index_offset : 0; const reversed = (action.payload.reversed) ? action.payload.reversed : true; return this.httpClient.get(this.CHILD_API_URL + environment.INVOICES_API + '?num_max_invoices=' + num_max_invoices + '&index_offset=' + index_offset + '&reversed=' + reversed). pipe( map((res: ListInvoices) => { this.logger.info(res); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchInvoices', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_INVOICES_CLN, payload: res }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchInvoices', UI_MESSAGES.NO_SPINNER, 'Fetching Invoices Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); offersFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_OFFERS_CLN), mergeMap((action: { type: string, payload: any }) => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchOffers', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.OFFERS_API). pipe(map((res: any) => { this.logger.info(res); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchOffers', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_OFFERS_CLN, payload: res.offers ? res.offers : [] }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchOffers', UI_MESSAGES.NO_SPINNER, 'Fetching Offers Failed.', err); return of({ type: RTLActions.VOID }); })); }) )); offersDisableCL = createEffect(() => this.actions.pipe( ofType(CLNActions.DISABLE_OFFER_CLN), mergeMap((action: { type: string, payload: { offer_id: string } }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DISABLE_OFFER })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'DisableOffer', status: APICallStatusEnum.INITIATED } })); return this.httpClient.delete(this.CHILD_API_URL + environment.OFFERS_API + '/' + action.payload.offer_id). pipe(map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'DisableOffer', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DISABLE_OFFER })); this.store.dispatch(openSnackBar({ payload: 'Offer Disabled Successfully!' })); return { type: CLNActions.UPDATE_OFFER_CLN, payload: { offer: postRes } }; }), catchError((err: any) => { this.handleErrorWithoutAlert('DisableOffer', UI_MESSAGES.DISABLE_OFFER, 'Disabling Offer Failed.', err); return of({ type: RTLActions.VOID }); })); }) )); offerBookmarksFetchCL = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_OFFER_BOOKMARKS_CLN), mergeMap((action: { type: string, payload: any }) => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchOfferBookmarks', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.OFFERS_API + '/offerbookmarks'). pipe(map((res: any) => { this.logger.info(res); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchOfferBookmarks', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_OFFER_BOOKMARKS_CLN, payload: res || [] }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchOfferBookmarks', UI_MESSAGES.NO_SPINNER, 'Fetching Offer Bookmarks Failed.', err); return of({ type: RTLActions.VOID }); })); }) )); peidOffersDeleteCL = createEffect(() => this.actions.pipe( ofType(CLNActions.DELETE_OFFER_BOOKMARK_CLN), mergeMap((action: { type: string, payload: { bolt12: string } }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DELETE_OFFER_BOOKMARK })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'DeleteOfferBookmark', status: APICallStatusEnum.INITIATED } })); return this.httpClient.delete(this.CHILD_API_URL + environment.OFFERS_API + '/offerbookmark/' + action.payload.bolt12). pipe(map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'DeleteOfferBookmark', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DELETE_OFFER_BOOKMARK })); this.store.dispatch(openSnackBar({ payload: 'Offer Bookmark Deleted Successfully!' })); return { type: CLNActions.REMOVE_OFFER_BOOKMARK_CLN, payload: { bolt12: action.payload.bolt12 } }; }), catchError((err: any) => { this.handleErrorWithAlert('DeleteOfferBookmark', UI_MESSAGES.DELETE_OFFER_BOOKMARK, 'Deleting Offer Bookmark Failed.', this.CHILD_API_URL + environment.OFFERS_API + '/offerbookmark/' + action.payload.bolt12, err); return of({ type: RTLActions.VOID }); })); }) )); SetChannelTransactionCL = createEffect(() => this.actions.pipe( ofType(CLNActions.SET_CHANNEL_TRANSACTION_CLN), mergeMap((action: { type: string, payload: OnChain }) => { this.store.dispatch(openSpinner({ payload: UI_MESSAGES.SEND_FUNDS })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SetChannelTransaction', status: APICallStatusEnum.INITIATED } })); return this.httpClient.post(this.CHILD_API_URL + environment.ON_CHAIN_API, action.payload). pipe( map((postRes: any) => { this.logger.info(postRes); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SetChannelTransaction', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.SEND_FUNDS })); this.store.dispatch(fetchBalance()); this.store.dispatch(fetchUTXOs()); return { type: CLNActions.SET_CHANNEL_TRANSACTION_RES_CLN, payload: postRes }; }), catchError((err: any) => { this.handleErrorWithoutAlert('SetChannelTransaction', UI_MESSAGES.SEND_FUNDS, 'Sending Fund Failed.', err); return of({ type: RTLActions.VOID }); }) ); }) )); utxosFetch = createEffect(() => this.actions.pipe( ofType(CLNActions.FETCH_UTXOS_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchUTXOs', status: APICallStatusEnum.INITIATED } })); return this.httpClient.get(this.CHILD_API_URL + environment.ON_CHAIN_API + '/utxos'); }), map((utxos: any) => { this.logger.info(utxos); this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchUTXOs', status: APICallStatusEnum.COMPLETED } })); return { type: CLNActions.SET_UTXOS_CLN, payload: utxos.outputs || [] }; }), catchError((err: any) => { this.handleErrorWithoutAlert('FetchUTXOs', UI_MESSAGES.NO_SPINNER, 'Fetching UTXOs Failed.', err); return of({ type: RTLActions.VOID }); }) )); initializeRemainingData(info: any, landingPage: string) { this.sessionService.setItem('clUnlocked', 'true'); const node_data = { identity_pubkey: info.id, alias: info.alias, testnet: (info.network.toLowerCase() === 'testnet'), chains: info.chains, uris: info.uris, version: info.version, api_version: info.api_version, numberOfPendingChannels: info.num_pending_channels }; this.store.dispatch(openSpinner({ payload: UI_MESSAGES.INITALIZE_NODE_DATA })); this.store.dispatch(setNodeData({ payload: node_data })); let newRoute = this.location.path(); if (newRoute.includes('/lnd/')) { newRoute = newRoute?.replace('/lnd/', '/cln/'); } else if (newRoute.includes('/ecl/')) { newRoute = newRoute?.replace('/ecl/', '/cln/'); } if (newRoute.includes('/login') || newRoute.includes('/error') || newRoute === '' || landingPage === 'HOME' || newRoute.includes('?access-key=')) { newRoute = '/cln/home'; } this.router.navigate([newRoute]); this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: 1000000, index_offset: 0, reversed: true } })); this.store.dispatch(fetchFees()); this.store.dispatch(fetchChannels()); this.store.dispatch(fetchBalance()); this.store.dispatch(fetchLocalRemoteBalance()); this.store.dispatch(fetchFeeRates({ payload: 'perkw' })); this.store.dispatch(fetchFeeRates({ payload: 'perkb' })); this.store.dispatch(fetchPeers()); this.store.dispatch(fetchUTXOs()); this.store.dispatch(fetchPayments()); } handleErrorWithoutAlert(actionName: string, uiMessage: string, genericErrorMessage: string, err: { status: number, error: any }) { this.logger.error('ERROR IN: ' + actionName + '\n' + JSON.stringify(err)); if (err.status === 401) { this.logger.info('Redirecting to Login'); this.store.dispatch(closeAllDialogs()); this.store.dispatch(logout()); this.store.dispatch(openSnackBar({ payload: 'Authentication Failed. Redirecting to Login.' })); } else { this.store.dispatch(closeSpinner({ payload: uiMessage })); const errMsg = this.commonService.extractErrorMessage(err, genericErrorMessage); this.store.dispatch(updateCLAPICallStatus({ payload: { action: actionName, status: APICallStatusEnum.ERROR, statusCode: err.status.toString(), message: errMsg } })); } } handleErrorWithAlert(actionName: string, uiMessage: string, alertTitle: string, errURL: string, err: { status: number, error: any }) { this.logger.error(err); if (err.status === 401) { this.logger.info('Redirecting to Login'); this.store.dispatch(closeAllDialogs()); this.store.dispatch(logout()); this.store.dispatch(openSnackBar({ payload: 'Authentication Failed. Redirecting to Login.' })); } else { this.store.dispatch(closeSpinner({ payload: uiMessage })); const errMsg = this.commonService.extractErrorMessage(err); this.store.dispatch(openAlert({ payload: { data: { type: 'ERROR', alertTitle: alertTitle, message: { code: err.status, message: errMsg, URL: errURL }, component: ErrorMessageComponent } } })); this.store.dispatch(updateCLAPICallStatus({ payload: { action: actionName, status: APICallStatusEnum.ERROR, statusCode: err.status.toString(), message: errMsg, URL: errURL } })); } } ngOnDestroy() { this.unSubs.forEach((completeSub) => { completeSub.next(null); completeSub.complete(); }); } }