UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

202 lines 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.confirmationEpic = exports.contractSettleTimeoutEpic = exports.blockStaleEpic = exports.blockTimeEpic = exports.initNewBlockEpic = exports.initEpic = void 0; const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const actions_1 = require("../../actions"); const config_1 = require("../../config"); const ethers_1 = require("../../utils/ethers"); const rx_1 = require("../../utils/rx"); const types_1 = require("../../utils/types"); const actions_2 = require("../actions"); /** * Emits raidenSynced when all init$ tasks got completed * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.init$ - Init$ subject * @returns Observable of raidenSynced actions */ function initEpic({}, state$, { init$ }) { return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(({ blockNumber: initialBlock }) => { const startTime = Date.now(); return init$.pipe((0, operators_1.mergeMap)((subject) => (0, rxjs_1.concat)((0, rxjs_1.of)(1), subject.pipe((0, operators_1.ignoreElements)(), (0, operators_1.endWith)(-1)))), (0, operators_1.scan)((acc, v) => acc + v, 0), // scan doesn't emit initial value (0, operators_1.debounceTime)(10), // should be just enough for some sync action (0, operators_1.first)((acc) => acc === 0), (0, operators_1.withLatestFrom)(state$), (0, operators_1.map)(([, { blockNumber }]) => (0, actions_1.raidenSynced)({ tookMs: Date.now() - startTime, initialBlock, currentBlock: blockNumber, }))); }), (0, rx_1.completeWith)(state$), (0, operators_1.finalize)(() => init$.complete())); } exports.initEpic = initEpic; /** * Fetch current blockNumber, register for new block events and emit newBlock actions * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.provider - Eth provider * @param deps.init$ - Observable which completes when initial sync is done * @returns Observable of newBlock actions */ function initNewBlockEpic(action$, {}, { provider, init$ }) { return (0, rx_1.retryAsync$)(() => provider.getBlockNumber(), provider.pollingInterval).pipe( // emits fetched block first, then subscribes to provider's block after synced (0, operators_1.mergeMap)((blockNumber) => init$.pipe((0, rx_1.lastMap)(() => (0, ethers_1.fromEthersEvent)(provider, 'block')), (0, operators_1.startWith)(blockNumber))), (0, operators_1.map)((blockNumber) => (0, actions_2.newBlock)({ blockNumber })), (0, rx_1.completeWith)(action$)); } exports.initNewBlockEpic = initNewBlockEpic; /** * Fetch and calculate average blockTime every fetchEach=20, across maxSize=5 requests, * i.e. moving average of 20*5=100 last blocks * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.getBlockTimestamp - Block timestamp (cached) getter function * @param deps.log - Logger instance * @returns Observable of blockTime actions */ function blockTimeEpic(action$, {}, { log, getBlockTimestamp }) { const fetchEach = 20; // how often to reevaluate const maxSize = 5; // max queue size const queue = []; // queue of past fetched (cached) block timestamps to reuse return action$.pipe((0, operators_1.filter)(actions_2.newBlock.is), (0, operators_1.pluck)('payload', 'blockNumber'), (0, operators_1.filter)((blockNumber) => !queue.length || queue[queue.length - 1] + fetchEach <= blockNumber), (0, operators_1.exhaustMap)((blockNumber) => { let pastBlock; if (queue.length < maxSize) pastBlock = Math.max(1, blockNumber - fetchEach * maxSize); else pastBlock = queue[0]; // use front, but pop only if successfully fetched return (0, rxjs_1.combineLatest)([getBlockTimestamp(blockNumber), getBlockTimestamp(pastBlock)]).pipe((0, operators_1.filter)(([curTs, pastTs]) => pastTs < curTs), (0, operators_1.map)(([curTs, pastTs]) => { // in case of success and queue is full, pop_front pastNumber if (queue.length >= maxSize) queue.splice(0, 1); queue.push(blockNumber); // then push_back new blockNumber return ((curTs - pastTs) * 1e3) / (blockNumber - pastBlock); }), (0, rx_1.catchAndLog)({ log: log.warn })); }), (0, operators_1.distinctUntilChanged)(), (0, operators_1.map)((avgBlockTime) => (0, actions_2.blockTime)({ blockTime: avgBlockTime }))); } exports.blockTimeEpic = blockTimeEpic; /** * Monitors provider for staleness. A provider is considered stale when it doesn't emit new blocks * on either 2 * httpTimeout or the average time for 3 blocks. * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.config$ - Config observable * @param deps.latest$ - Latest observable * @param deps.init$ - Init observable * @returns Observable of blockStale actions */ function blockStaleEpic({}, state$, { latest$, config$, init$ }) { return state$.pipe((0, operators_1.skipUntil)(init$.pipe((0, operators_1.last)())), (0, rx_1.pluckDistinct)('blockNumber'), (0, operators_1.withLatestFrom)(latest$, config$), // forEach block (0, operators_1.map)(([, { blockTime }, { httpTimeout }]) => Math.max(3 * blockTime, 2 * httpTimeout)), // switchMap will "reset" timer every block, restarting the timeout (0, operators_1.switchMap)((staleTimeout) => (0, rxjs_1.concat)((0, rxjs_1.of)(false), (0, rxjs_1.timer)(staleTimeout).pipe((0, operators_1.mapTo)(true), // ensure timer completes output if input completes, // but first element of concat ensures it'll emit at least once (true) when subscribed (0, rx_1.completeWith)(state$)))), (0, operators_1.distinctUntilChanged)(), (0, operators_1.map)((stale) => (0, actions_2.blockStale)({ stale }))); } exports.blockStaleEpic = blockStaleEpic; /** * Fetch settleTimeout from contract once * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.log - Logger instance * @param deps.registryContract - TokenNetworkRegistry instance * @param deps.config$ - Config observable * @param deps.init$ - Observable of sync tasks * @returns Observable of contractSettleTimeout actions */ function contractSettleTimeoutEpic({}, state$, { log, registryContract, config$, init$ }) { let done$; return (0, rxjs_1.defer)(async () => { done$ = new rxjs_1.AsyncSubject(); init$.next(done$); return registryContract.callStatic.settle_timeout(); }).pipe((0, operators_1.map)((settleTimeout) => settleTimeout.toNumber()), (0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$)), (0, operators_1.withLatestFrom)(state$, config$), (0, operators_1.mergeMap)(function* ([settleTimeout, { config: userConfig }, { revealTimeout }]) { yield (0, actions_2.contractSettleTimeout)(settleTimeout); if (revealTimeout <= settleTimeout / 2) return; if (!('revealTimeout' in userConfig)) { // in case user hasn't explicitly set config.revealTimeout, choose a sane default yield (0, actions_1.raidenConfigUpdate)({ revealTimeout: Math.floor(settleTimeout / 2) }); } else { // otherwise, warn but don't error to allow them to reset it log.warn('Invalid `config.revealTimeout` - transfers may fail', { revealTimeout, maxRevealTimeout: settleTimeout / 2, }); } }), (0, operators_1.tap)(() => { done$.next(null); done$.complete(); })); } exports.contractSettleTimeoutEpic = contractSettleTimeoutEpic; function checkPendingAction(action, provider, blockNumber, confirmationBlocks) { return (0, rx_1.retryAsync$)(() => provider.getTransactionReceipt(action.payload.txHash), provider.pollingInterval).pipe((0, operators_1.map)((receipt) => { if (receipt?.confirmations !== undefined && receipt.confirmations >= confirmationBlocks && receipt.status // reorgs can make txs fail ) { return { ...action, // beyond setting confirmed, also re-set blockNumber, // which may have changed on a reorg payload: { ...action.payload, txBlock: receipt.blockNumber ?? action.payload.txBlock, confirmed: true, }, }; } else if (action.payload.txBlock + 2 * confirmationBlocks < blockNumber) { // if this txs didn't get confirmed for more than 2*confirmationBlocks, it was removed return { ...action, payload: { ...action.payload, confirmed: false }, }; } // else, it seems removed, but give it twice confirmationBlocks to be picked up again }), (0, operators_1.filter)(types_1.isntNil)); } /** * Process new blocks and re-emit confirmed or removed actions * * Events can also be confirmed by `fromEthersEvent + map(logToContractEvent)` combination. * Notice that this epic does not know how to parse a tx log to update an action which payload was * composed of values which can change upon reorgs. It only checks if given txHash is still present * on the blockchain. `fromEthersEvent` can usually emit unconfirmed events multiple times to * update/replace the pendingTxs action if needed, and also should emit the confirmed action with * proper values; therefore, one should only relay on this epic to confirm an action if there's * nothing critical depending on values in it's payload which can change upon reorgs. * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.config$ - Config observable * @param deps.provider - Eth provider * @param deps.latest$ - Latest observable * @returns Observable of confirmed or removed actions */ function confirmationEpic({}, state$, { config$, provider, latest$ }) { return (0, rxjs_1.combineLatest)([ state$.pipe((0, rx_1.pluckDistinct)('blockNumber')), state$.pipe((0, operators_1.pluck)('pendingTxs')), config$.pipe((0, rx_1.pluckDistinct)('confirmationBlocks'), (0, rx_1.completeWith)(state$)), ]).pipe((0, operators_1.filter)(([, pendingTxs]) => pendingTxs.length > 0), // exhaust will ignore blocks while concat$ is busy (0, operators_1.exhaustMap)(([blockNumber, pendingTxs, confirmationBlocks]) => (0, rxjs_1.from)(pendingTxs).pipe( // only txs/confirmable actions which are more than confirmationBlocks in the past (0, operators_1.filter)((a) => a.payload.txBlock + confirmationBlocks < blockNumber), (0, operators_1.concatMap)((action) => checkPendingAction(action, provider, blockNumber, confirmationBlocks).pipe( // unsubscribe if it gets cleared from 'pendingTxs' while checking, to avoid duplicate (0, operators_1.takeUntil)(latest$.pipe((0, operators_1.filter)(({ state }) => !state.pendingTxs.some((a) => a.type === action.type && a.payload.txHash === action.payload.txHash))))))))); } exports.confirmationEpic = confirmationEpic; //# sourceMappingURL=block.js.map