UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

256 lines 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.channelUnlockEpic = exports.channelSettleEpic = exports.channelAutoSettleEpic = exports.channelSettleableEpic = void 0; const bytes_1 = require("@ethersproject/bytes"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const config_1 = require("../../config"); const utils_1 = require("../../messages/utils"); const utils_2 = require("../../transfers/epics/utils"); const state_1 = require("../../transfers/state"); const utils_3 = require("../../transfers/utils"); const utils_4 = require("../../transport/utils"); const actions_1 = require("../../utils/actions"); const data_1 = require("../../utils/data"); const error_1 = require("../../utils/error"); const rx_1 = require("../../utils/rx"); const actions_2 = require("../actions"); const state_2 = require("../state"); const utils_5 = require("../utils"); /** * Process newBlocks, emits ChannelSettleableAction if any closed channel is now settleable * * @param action$ - Observable of newBlock actions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.getBlockTimestamp - Block's timestamp getter function * @param deps.latest$ - Latest observable * @returns Observable of channelSettleable actions */ function channelSettleableEpic({}, state$, { getBlockTimestamp, latest$ }) { return state$.pipe((0, utils_5.groupChannel)(), // for each channel's observable (0, operators_1.mergeMap)((grouped$) => grouped$.pipe((0, operators_1.filter)((channel) => channel.state === state_2.ChannelState.closed), // once detecting a closed channel, delay emit until after settleTimeout window after close (0, operators_1.take)(1), (0, operators_1.delayWhen)((channel) => getBlockTimestamp(channel.closeBlock).pipe((0, operators_1.withLatestFrom)(latest$), (0, operators_1.mergeMap)(([closeTimestamp, { settleTimeout }]) => (0, rxjs_1.timer)(new Date((closeTimestamp + settleTimeout) * 1e3))))), (0, operators_1.withLatestFrom)(state$), (0, operators_1.map)(([channel, { blockNumber: currentBlock }]) => (0, actions_2.channelSettleable)({ settleableBlock: currentBlock + 1 }, { tokenNetwork: channel.tokenNetwork, partner: channel.partner.address }))))); } exports.channelSettleableEpic = channelSettleableEpic; /** * If config.autoSettle is true, calls channelSettle.request on settleable channels * If partner is a LC and not the closing side, they're expected to wait [config.revealTimeout] * after channel becomes settleable before attempting to auto-settle, so we should attempt it * earlier [config.confirmationBlocks] after settleable. PCs always attempt earlier, so we should * wait longer regardless of who closed the channel, to avoid races and wasted gas; if even after * waiting (earlier or later) channel isn't settling/settled, we do it anyway. * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.address - Our address * @param deps.config$ - Config observable * @returns Observable of channelSettle.request actions */ function channelAutoSettleEpic(action$, state$, { address, config$ }) { return state$.pipe((0, utils_5.groupChannel)(), // for each channel's observable (0, operators_1.withLatestFrom)(action$.pipe((0, utils_4.getPresencesByAddress)())), (0, operators_1.mergeMap)(([grouped$, presences]) => grouped$.pipe((0, operators_1.filter)((channel) => channel.state === state_2.ChannelState.settleable), (0, operators_1.take)(1), (0, operators_1.withLatestFrom)(config$), (0, operators_1.delayWhen)(([channel, { httpTimeout, revealTimeout }]) => { const partnerIsOnlineLC = (0, utils_4.peerIsOnlineLC)(presences[channel.partner.address]); /* iff we are *sure* partner will wait longer (i.e. they're a LC and *we* are the closing * side), then we autoSettle early; otherwise (it's a PC or they're the closing side), * we can wait longer before autoSettling */ let waitTime; if (partnerIsOnlineLC && channel.closeParticipant === address) waitTime = httpTimeout; else waitTime = revealTimeout * 1e3; return (0, rxjs_1.timer)(waitTime); }), (0, operators_1.map)(([channel]) => actions_2.channelSettle.request(undefined, { tokenNetwork: channel.tokenNetwork, partner: channel.partner.address, })))), // ensures only one auto request is handled at a time (0, operators_1.concatMap)((request) => (0, utils_2.dispatchAndWait$)(action$, request, (0, actions_1.isResponseOf)(actions_2.channelSettle, request.meta))), (0, rx_1.takeIf)(config$.pipe((0, operators_1.pluck)('autoSettle'), (0, rx_1.completeWith)(state$)))); } exports.channelAutoSettleEpic = channelAutoSettleEpic; function settleSettleableChannel([action$, action], channel, deps) { const { address, db, config$, log, getTokenNetworkContract } = deps; const { tokenNetwork, partner } = action.meta; const tokenNetworkContract = getTokenNetworkContract(tokenNetwork); // fetch closing/updated balanceHash for each end return (0, rxjs_1.defer)(() => Promise.all([ tokenNetworkContract.callStatic.getChannelParticipantInfo(channel.id, address, partner), tokenNetworkContract.callStatic.getChannelParticipantInfo(channel.id, partner, address), ])).pipe((0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { onErrors: error_1.networkErrors }), (0, operators_1.mergeMap)(([{ 3: ownBH }, { 3: partnerBH }]) => { let ownBP$; if (ownBH === (0, utils_1.createBalanceHash)(channel.own.balanceProof)) { ownBP$ = (0, rxjs_1.of)(channel.own.balanceProof); } else { // partner closed/updated the channel with a non-latest BP from us // they would lose our later transfers, but to settle we must search transfer history ownBP$ = (0, utils_3.findBalanceProofMatchingBalanceHash$)(db, channel, state_1.Direction.SENT, ownBH).pipe((0, operators_1.catchError)(() => { throw new error_1.RaidenError(error_1.ErrorCodes.CNL_SETTLE_INVALID_BALANCEHASH, { address, ownBalanceHash: ownBH, }); })); } let partnerBP$; if (partnerBH === (0, utils_1.createBalanceHash)(channel.partner.balanceProof)) { partnerBP$ = (0, rxjs_1.of)(channel.partner.balanceProof); } else { // shouldn't happen, since it's expected we were the closing part partnerBP$ = (0, utils_3.findBalanceProofMatchingBalanceHash$)(db, channel, state_1.Direction.RECEIVED, partnerBH).pipe((0, operators_1.catchError)(() => { throw new error_1.RaidenError(error_1.ErrorCodes.CNL_SETTLE_INVALID_BALANCEHASH, { address, partnerBalanceHash: partnerBH, }); })); } // send settleChannel transaction return (0, rxjs_1.combineLatest)([ownBP$, partnerBP$]).pipe((0, operators_1.map)(([ownBP, partnerBP]) => { // part1 total amounts must be <= part2 total amounts on settleChannel call if (partnerBP.transferredAmount .add(partnerBP.lockedAmount) .lt(ownBP.transferredAmount.add(ownBP.lockedAmount))) return [ [partner, partnerBP], [address, ownBP], ]; else return [ [address, ownBP], [partner, partnerBP], ]; }), (0, operators_1.mergeMap)(([part1, part2]) => (0, utils_5.transact)(tokenNetworkContract, 'settleChannel', [ channel.id, part1[0], part1[1].transferredAmount, part1[1].lockedAmount, part1[1].locksroot, part2[0], part2[1].transferredAmount, part2[1].lockedAmount, part2[1].locksroot, ], deps, { error: error_1.ErrorCodes.CNL_SETTLE_FAILED })), (0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { maxRetries: 3, onErrors: error_1.commonAndFailTxErrors, log: log.info, }), // if channel gets settled while retrying (e.g. by partner), give up (0, operators_1.takeUntil)(action$.pipe((0, operators_1.filter)(actions_2.channelSettle.success.is), (0, operators_1.filter)((action) => action.meta.tokenNetwork === tokenNetwork && action.meta.partner === partner)))); }), // if succeeded, return a empty/completed observable // actual ChannelSettledAction will be detected and handled by channelEventsEpic // if any error happened on tx call/pipeline, mergeMap below won't be hit, and catchError // will then emit the channelSettle.failure action instead (0, operators_1.ignoreElements)(), (0, operators_1.catchError)((error) => (0, rxjs_1.of)(actions_2.channelSettle.failure(error, action.meta)))); } function withdrawPairToCoopSettleParams([req, conf]) { return { participant: req.participant, total_withdraw: req.total_withdraw, withdrawable_until: req.expiration, participant_signature: req.signature, partner_signature: conf.signature, }; } function coopSettleChannel([action$, action], channel, deps) { const { config$, log, getTokenNetworkContract } = deps; const { tokenNetwork, partner } = action.meta; const [part1, part2] = action.payload.coopSettle; // fetch closing/updated balanceHash for each end return (0, utils_5.transact)(getTokenNetworkContract(tokenNetwork), 'cooperativeSettle', [channel.id, withdrawPairToCoopSettleParams(part1), withdrawPairToCoopSettleParams(part2)], deps, { error: error_1.ErrorCodes.CNL_COOP_SETTLE_FAILED }).pipe((0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { maxRetries: 3, onErrors: error_1.commonAndFailTxErrors, log: log.info, }), // if channel gets settled while retrying (e.g. by partner), give up (0, operators_1.takeUntil)(action$.pipe((0, operators_1.filter)(actions_2.channelSettle.success.is), (0, operators_1.filter)((action) => action.meta.tokenNetwork === tokenNetwork && action.meta.partner === partner))), // if succeeded, return a empty/completed observable // actual ChannelSettledAction will be detected and handled by channelEventsEpic // if any error happened on tx call/pipeline, mergeMap below won't be hit, and catchError // will then emit the channelSettle.failure action instead (0, operators_1.ignoreElements)(), (0, operators_1.catchError)((error) => (0, rxjs_1.of)(actions_2.channelSettle.failure(error, action.meta)))); } /** * A ChannelSettle action requested by user * Needs to be called on an settleable or settling (for retries) channel. * If tx goes through successfuly, stop as ChannelSettled success action will instead be detected * and reacted by channelEventsEpic. * If anything detectable goes wrong, fires a ChannelSettleActionFailed instead * * @param action$ - Observable of channelSettle actions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.log - Logger instance * @param deps.signer - Signer instance * @param deps.address - Our address * @param deps.main - Main signer/address * @param deps.provider - Provider instance * @param deps.getTokenNetworkContract - TokenNetwork contract instance getter * @param deps.config$ - Config observable * @param deps.latest$ - Latest observable * @param deps.db - Database instance * @returns Observable of channelSettle.failure actions */ function channelSettleEpic(action$, state$, deps) { return action$.pipe((0, operators_1.filter)(actions_2.channelSettle.request.is), (0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([action, state]) => { const channel = state.channels[(0, utils_5.channelKey)(action.meta)]; if (channel?.state === state_2.ChannelState.settleable || channel?.state === state_2.ChannelState.settling) { return settleSettleableChannel([action$, action], channel, deps); } else if (action.payload?.coopSettle && (channel?.state === state_2.ChannelState.open || channel?.state === state_2.ChannelState.closing)) { return coopSettleChannel([action$, action], channel, deps); } else { return (0, rxjs_1.of)(actions_2.channelSettle.failure(new error_1.RaidenError(error_1.ErrorCodes.CNL_NO_SETTLEABLE_OR_SETTLING_CHANNEL_FOUND, action.meta), action.meta)); } })); } exports.channelSettleEpic = channelSettleEpic; /** * When channel is settled, unlock any pending lock on-chain * TODO: check if it's worth it to also unlock partner's end * TODO: do it only if economically viable (and define what that means) * * @param action$ - Observable of channelSettle.success actions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps members * @param deps.log - Logger instance * @param deps.signer - Signer instance * @param deps.address - Our address * @param deps.main - Main signer/address * @param deps.provider - Provider instance * @param deps.getTokenNetworkContract - TokenNetwork contract instance getter * @param deps.config$ - Config observable * @param deps.latest$ - Latest observable * @returns Empty observable */ function channelUnlockEpic(action$, state$, deps) { const { log, address, getTokenNetworkContract, config$ } = deps; return action$.pipe((0, operators_1.filter)((0, actions_1.isActionOf)(actions_2.channelSettle.success)), (0, operators_1.filter)((action) => !!(action.payload.confirmed && action.payload.locks?.length)), (0, operators_1.withLatestFrom)(state$), // ensure there's no channel, or if yes, it's a different (by channelId) (0, operators_1.filter)(([action, state]) => state.channels[(0, utils_5.channelKey)(action.meta)]?.id !== action.payload.id), (0, operators_1.mergeMap)(([action]) => { const { tokenNetwork, partner } = action.meta; const locks = (0, bytes_1.concat)(action.payload.locks.reduce((acc, lock) => [ ...acc, (0, data_1.encode)(lock.expiration, 32), (0, data_1.encode)(lock.amount, 32), lock.secrethash, ], [])); // send unlock transaction return (0, utils_5.transact)(getTokenNetworkContract(tokenNetwork), 'unlock', [action.payload.id, address, partner, locks], deps, { error: error_1.ErrorCodes.CNL_ONCHAIN_UNLOCK_FAILED }).pipe((0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { maxRetries: 3, onErrors: error_1.commonAndFailTxErrors, log: log.info, }), (0, operators_1.ignoreElements)(), (0, operators_1.catchError)((error) => { log.error('Error unlocking pending locks on-chain, ignoring', error); return rxjs_1.EMPTY; })); })); } exports.channelUnlockEpic = channelUnlockEpic; //# sourceMappingURL=settle.js.map