raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
255 lines • 14.6 kB
JavaScript
;
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