UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

255 lines 14.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.combineRaidenEpics = exports.getLatest$ = void 0; const constants_1 = require("@ethersproject/constants"); const fp_1 = require("lodash/fp"); const unset_1 = __importDefault(require("lodash/fp/unset")); const isEqual_1 = __importDefault(require("lodash/isEqual")); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const actions_1 = require("./actions"); const actions_2 = require("./channels/actions"); const ChannelsEpics = __importStar(require("./channels/epics")); const constants_2 = require("./constants"); const DatabaseEpics = __importStar(require("./db/epics")); const actions_3 = require("./services/actions"); const ServicesEpics = __importStar(require("./services/epics")); const TransfersEpics = __importStar(require("./transfers/epics")); const actions_4 = require("./transport/actions"); const TransportEpics = __importStar(require("./transport/epics")); const rx_1 = require("./utils/rx"); const types_1 = require("./utils/types"); // default values for dynamic capabilities not specified on defaultConfig nor userConfig function dynamicCaps({ stale, udcDeposit, config: { monitoringReward }, }) { return { [constants_2.Capabilities.RECEIVE]: !stale && monitoringReward?.gt(0) && monitoringReward.lte(udcDeposit.balance) ? 1 : 0, [constants_2.Capabilities.WEBRTC]: 'RTCPeerConnection' in globalThis ? 1 : 0, }; } function mergeCaps(dynamicCaps, defaultCaps, userCaps) { // if userCaps is disabled, disables everything if (userCaps === null) return userCaps; // if userCaps is an object, merge all caps else if (userCaps !== undefined) return { ...dynamicCaps, ...defaultCaps, ...userCaps }; // if userCaps isn't set and defaultCaps is null, disables everything else if (defaultCaps === null) return defaultCaps; // if userCaps isn't set and defaultCaps is an object, merge it with dynamicCaps else return { ...dynamicCaps, ...defaultCaps }; } /** * Aggregate dynamic (runtime-values dependent), default and user capabilities and emit * raidenConfigCaps actions when it changes * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.defaultConfig - Default config object * @param deps.latest$ - latest observable * @returns Observable of raidenConfigCaps actions */ function configCapsEpic({}, state$, { defaultConfig, latest$ }) { return (0, rxjs_1.combineLatest)([state$.pipe((0, rx_1.pluckDistinct)('config', 'caps')), latest$]).pipe((0, operators_1.map)(([userCaps, latest]) => mergeCaps(dynamicCaps(latest), defaultConfig.caps, userCaps)), (0, operators_1.distinctUntilChanged)(isEqual_1.default), (0, operators_1.map)((caps) => (0, actions_1.raidenConfigCaps)({ caps })), (0, rx_1.completeWith)(state$)); } /** * React on certain config property changes and act accordingly: * Currently, reflect config.logger on deps.log's level, and config.pollingInterval on provider's * pollingInterval. * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.config$ - Config observable * @param deps.log - Logger instance * @param deps.provider - Provider instance * @returns Observable which never emits */ function configReactEpic(action$, {}, { config$, log, provider }) { return (0, rxjs_1.merge)(config$.pipe((0, rx_1.pluckDistinct)('logger'), (0, operators_1.tap)((level) => log.setLevel(level || 'silent', false))), config$.pipe((0, rx_1.pluckDistinct)('pollingInterval'), (0, operators_1.tap)((pollingInterval) => (provider.pollingInterval = pollingInterval)))).pipe((0, operators_1.ignoreElements)(), (0, rx_1.completeWith)(action$)); } const ConfigEpics = { configCapsEpic, configReactEpic }; /** * This function maps cached/latest relevant values from action$ & state$ * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies, minus 'latest$' & 'config$' (outputs) * @param deps.defaultConfig - defaultConfig mapping * @param deps.mediationFeeCalculator - Calculator used to decode/validate config.mediationFees * @returns latest$ observable */ function getLatest$(action$, state$, // do not use latest$ or dependents (e.g. config$), as they're defined here { defaultConfig, mediationFeeCalculator, }) { const initialUdcDeposit = { balance: constants_1.MaxUint256, totalDeposit: constants_1.MaxUint256, }; const initialStale = false; const udcDeposit$ = action$.pipe((0, operators_1.filter)(actions_3.udcDeposit.success.is), (0, operators_1.filter)((action) => !('confirmed' in action.payload) || !!action.payload.confirmed), (0, operators_1.map)((action) => ({ balance: action.payload.balance, totalDeposit: action.meta.totalDeposit })), // starts with max, to prevent receiving starting as disabled before actual balance is fetched (0, operators_1.startWith)(initialUdcDeposit), (0, operators_1.distinctUntilChanged)(({ balance: a }, { balance: b }) => a.eq(b))); const blockTime$ = action$.pipe((0, operators_1.filter)(actions_2.blockTime.is), (0, operators_1.pluck)('payload', 'blockTime'), (0, operators_1.startWith)(15e3)); const stale$ = action$.pipe((0, operators_1.filter)(actions_2.blockStale.is), (0, operators_1.pluck)('payload', 'stale'), (0, operators_1.startWith)(initialStale)); const settleTimeout$ = action$.pipe((0, operators_1.filter)(actions_2.contractSettleTimeout.is), (0, operators_1.pluck)('payload'), (0, operators_1.startWith)(15000)); const caps$ = (0, rxjs_1.merge)(state$.pipe((0, operators_1.take)(1), // initial caps depends on first state$ emit (initial) (0, operators_1.pluck)('config'), (0, operators_1.map)(({ caps: userCaps, monitoringReward }) => mergeCaps(dynamicCaps({ udcDeposit: initialUdcDeposit, stale: initialStale, config: { monitoringReward: monitoringReward ?? defaultConfig.monitoringReward }, }), defaultConfig.caps, userCaps))), // after that, pick from raidenConfigCaps actions action$.pipe((0, operators_1.filter)(actions_1.raidenConfigCaps.is), (0, operators_1.pluck)('payload', 'caps'))); const config$ = (0, rxjs_1.combineLatest)([state$.pipe((0, rx_1.pluckDistinct)('config')), caps$]).pipe((0, operators_1.map)(([userConfig, caps]) => ({ ...defaultConfig, ...userConfig, caps, mediationFees: mediationFeeCalculator.decodeConfig(userConfig.mediationFees, defaultConfig.mediationFees), }))); const whitelisted$ = state$.pipe((0, operators_1.take)(1), (0, operators_1.mergeMap)((initialState) => { const initialPartners = (0, fp_1.uniq)(Object.values(initialState.channels).map(({ partner }) => partner.address)); return action$.pipe((0, operators_1.filter)(actions_4.matrixPresence.request.is), (0, operators_1.scan)((whitelist, request) => whitelist.includes(request.meta.address) ? whitelist : [...whitelist, request.meta.address], initialPartners), (0, operators_1.startWith)(initialPartners), (0, operators_1.distinctUntilChanged)()); })); const rtc$ = action$.pipe((0, operators_1.filter)(actions_4.rtcChannel.is), // scan: if v.payload is defined, set it; else, unset (0, operators_1.scan)((acc, v) => v.payload ? { ...acc, [v.meta.address]: v.payload } : (0, unset_1.default)(v.meta.address, acc), {}), (0, operators_1.startWith)({})); return (0, rxjs_1.combineLatest)([ action$, state$, config$, whitelisted$, rtc$, udcDeposit$, blockTime$, stale$, settleTimeout$, ]).pipe((0, operators_1.map)(([action, state, config, whitelisted, rtc, udcDeposit, blockTime, stale, settleTimeout,]) => ({ action, state, config, whitelisted, rtc, udcDeposit, blockTime, stale, settleTimeout, }))); } exports.getLatest$ = getLatest$; /** * Pipes getLatest$ to deps.latest$; this is a special epic, which should be the first subscribed, * in order to update deps.latest$ before all other epics receive new notifications, but last * unsubscribed, so any shutdown emitted value will update latest$ till the end; * ensure deps.latest$ is completed even on unsubscription. * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @returns Observable of never */ function latestEpic(action$, state$, deps) { return getLatest$(action$, state$, deps).pipe((0, operators_1.tap)(deps.latest$), (0, operators_1.finalize)(deps.latest$.complete.bind(deps.latest$)), (0, operators_1.ignoreElements)()); } // Order matters! When shutting down, each epic's completion triggers the next to shut down, // meaning epics in this list should not assume previous epics are running, but can assume later // ones are (e.g. services epics may assume transport epics are still be subscribed, so messages // can be sent) const raidenEpics = { ...ConfigEpics, ...ChannelsEpics, ...TransfersEpics, ...ServicesEpics, ...TransportEpics, ...DatabaseEpics, }; /** * Consumes epics from epics$ and returns a root epic which properly wraps deps.latest$ and * limits action$ and state$ when raidenShutdown request goes through * * @param epics - Observable of raiden epics to compose the root epic * @returns The rootEpic which properly wires latest$ and limits action$ & state$ */ function combineRaidenEpics(epics = Object.values(raidenEpics)) { /** * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @returns Raiden root epic observable */ return function raidenRootEpic(action$, state$, deps) { const shutdownNotification$ = action$.pipe((0, operators_1.filter)(actions_1.raidenShutdown.is)); const subscribedChanged = new rxjs_1.BehaviorSubject([]); // main epics output; like combineEpics, but completes action$, state$ & output$ when a // raidenShutdown goes through const output$ = (0, rxjs_1.from)(epics).pipe((0, operators_1.startWith)(latestEpic), (0, operators_1.mergeMap)((epic) => { // latestEpic must be first subscribed, last shut down if (epic === latestEpic) subscribedChanged.next(subscribedChanged.value.concat(epic)); // insert epic in the end, just before latestEpic, so latestEpic gets shut down last else subscribedChanged.next(subscribedChanged.value.slice(0, -1).concat(epic, (0, types_1.last)(subscribedChanged.value))); // trigger each epic's shutdown after system's shutdown and after previous epic // completed (serial completion) const epicShutdown$ = subscribedChanged.pipe((0, operators_1.combineLatestWith)(shutdownNotification$), // re-emit/evaluate when shutdown (0, operators_1.skipUntil)(shutdownNotification$), // don't shutdown first epic if not yet shutting down (0, operators_1.filter)(([subscribed]) => subscribed[0] === epic)); return epic( // we shut down an epic by completing its inputs, then it should gracefully complete // whenever it can action$.pipe((0, operators_1.takeUntil)(epicShutdown$)), state$.pipe((0, operators_1.takeUntil)(epicShutdown$)), deps).pipe((0, operators_1.catchError)((err) => { deps.log.error('Epic error', epic.name, epic, err); return (0, rxjs_1.of)((0, actions_1.raidenShutdown)({ reason: err })); }), // but if an epic takes more than httpTimeout, forcefully completes it (0, operators_1.takeUntil)(epicShutdown$.pipe((0, operators_1.withLatestFrom)(deps.config$), // give up to httpTimeout for the epics to complete on their own (0, operators_1.delayWhen)(([_, { httpTimeout }]) => (0, rxjs_1.timer)(httpTimeout)), (0, operators_1.tap)(() => deps.log.warn('Epic stuck:', epic.name, epic)))), (0, operators_1.finalize)(() => { subscribedChanged.next(subscribedChanged.value.filter((v) => v !== epic)); })); }), // if a second shutdownNotification$ fires, unsubscribe inconditionally (0, operators_1.takeUntil)(shutdownNotification$.pipe((0, operators_1.skip)(1)))); // also concat db teardown tasks, to be done after main epic completes const teardown$ = deps.db.busy$.pipe((0, operators_1.first)((busy) => !busy), (0, operators_1.tap)(() => deps.db.busy$.next(true)), // ignore db.busy$ errors, they're merged in the output by dbErrorsEpic (0, operators_1.catchError)(() => (0, rxjs_1.of)(null)), (0, operators_1.mergeMap)(async () => deps.db.close()), (0, operators_1.ignoreElements)(), (0, operators_1.finalize)(() => { deps.db.busy$.next(false); deps.db.busy$.complete(); })); // subscribe to teardown$ only after output$ completes return (0, rxjs_1.concat)(output$, teardown$); }; } exports.combineRaidenEpics = combineRaidenEpics; //# sourceMappingURL=epics.js.map