UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

249 lines 13.7 kB
import { concat as concatBytes } from '@ethersproject/bytes'; import { combineLatest, defer, EMPTY, of, timer } from 'rxjs'; import { catchError, concatMap, delayWhen, filter, ignoreElements, map, mergeMap, pluck, take, takeUntil, withLatestFrom, } from 'rxjs/operators'; import { intervalFromConfig } from '../../config'; import { createBalanceHash } from '../../messages/utils'; import { dispatchAndWait$ } from '../../transfers/epics/utils'; import { Direction } from '../../transfers/state'; import { findBalanceProofMatchingBalanceHash$ } from '../../transfers/utils'; import { getPresencesByAddress, peerIsOnlineLC } from '../../transport/utils'; import { isActionOf, isResponseOf } from '../../utils/actions'; import { encode } from '../../utils/data'; import { commonAndFailTxErrors, ErrorCodes, networkErrors, RaidenError } from '../../utils/error'; import { completeWith, retryWhile, takeIf } from '../../utils/rx'; import { channelSettle, channelSettleable } from '../actions'; import { ChannelState } from '../state'; import { channelKey, groupChannel, transact } from '../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 */ export function channelSettleableEpic({}, state$, { getBlockTimestamp, latest$ }) { return state$.pipe(groupChannel(), // for each channel's observable mergeMap((grouped$) => grouped$.pipe(filter((channel) => channel.state === ChannelState.closed), // once detecting a closed channel, delay emit until after settleTimeout window after close take(1), delayWhen((channel) => getBlockTimestamp(channel.closeBlock).pipe(withLatestFrom(latest$), mergeMap(([closeTimestamp, { settleTimeout }]) => timer(new Date((closeTimestamp + settleTimeout) * 1e3))))), withLatestFrom(state$), map(([channel, { blockNumber: currentBlock }]) => channelSettleable({ settleableBlock: currentBlock + 1 }, { tokenNetwork: channel.tokenNetwork, partner: channel.partner.address }))))); } /** * 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 */ export function channelAutoSettleEpic(action$, state$, { address, config$ }) { return state$.pipe(groupChannel(), // for each channel's observable withLatestFrom(action$.pipe(getPresencesByAddress())), mergeMap(([grouped$, presences]) => grouped$.pipe(filter((channel) => channel.state === ChannelState.settleable), take(1), withLatestFrom(config$), delayWhen(([channel, { httpTimeout, revealTimeout }]) => { const partnerIsOnlineLC = 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 timer(waitTime); }), map(([channel]) => channelSettle.request(undefined, { tokenNetwork: channel.tokenNetwork, partner: channel.partner.address, })))), // ensures only one auto request is handled at a time concatMap((request) => dispatchAndWait$(action$, request, isResponseOf(channelSettle, request.meta))), takeIf(config$.pipe(pluck('autoSettle'), completeWith(state$)))); } 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 defer(() => Promise.all([ tokenNetworkContract.callStatic.getChannelParticipantInfo(channel.id, address, partner), tokenNetworkContract.callStatic.getChannelParticipantInfo(channel.id, partner, address), ])).pipe(retryWhile(intervalFromConfig(config$), { onErrors: networkErrors }), mergeMap(([{ 3: ownBH }, { 3: partnerBH }]) => { let ownBP$; if (ownBH === createBalanceHash(channel.own.balanceProof)) { ownBP$ = 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$ = findBalanceProofMatchingBalanceHash$(db, channel, Direction.SENT, ownBH).pipe(catchError(() => { throw new RaidenError(ErrorCodes.CNL_SETTLE_INVALID_BALANCEHASH, { address, ownBalanceHash: ownBH, }); })); } let partnerBP$; if (partnerBH === createBalanceHash(channel.partner.balanceProof)) { partnerBP$ = of(channel.partner.balanceProof); } else { // shouldn't happen, since it's expected we were the closing part partnerBP$ = findBalanceProofMatchingBalanceHash$(db, channel, Direction.RECEIVED, partnerBH).pipe(catchError(() => { throw new RaidenError(ErrorCodes.CNL_SETTLE_INVALID_BALANCEHASH, { address, partnerBalanceHash: partnerBH, }); })); } // send settleChannel transaction return combineLatest([ownBP$, partnerBP$]).pipe(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], ]; }), mergeMap(([part1, part2]) => 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: ErrorCodes.CNL_SETTLE_FAILED })), retryWhile(intervalFromConfig(config$), { maxRetries: 3, onErrors: commonAndFailTxErrors, log: log.info, }), // if channel gets settled while retrying (e.g. by partner), give up takeUntil(action$.pipe(filter(channelSettle.success.is), 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 ignoreElements(), catchError((error) => of(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 transact(getTokenNetworkContract(tokenNetwork), 'cooperativeSettle', [channel.id, withdrawPairToCoopSettleParams(part1), withdrawPairToCoopSettleParams(part2)], deps, { error: ErrorCodes.CNL_COOP_SETTLE_FAILED }).pipe(retryWhile(intervalFromConfig(config$), { maxRetries: 3, onErrors: commonAndFailTxErrors, log: log.info, }), // if channel gets settled while retrying (e.g. by partner), give up takeUntil(action$.pipe(filter(channelSettle.success.is), 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 ignoreElements(), catchError((error) => of(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 */ export function channelSettleEpic(action$, state$, deps) { return action$.pipe(filter(channelSettle.request.is), withLatestFrom(state$), mergeMap(([action, state]) => { const channel = state.channels[channelKey(action.meta)]; if (channel?.state === ChannelState.settleable || channel?.state === ChannelState.settling) { return settleSettleableChannel([action$, action], channel, deps); } else if (action.payload?.coopSettle && (channel?.state === ChannelState.open || channel?.state === ChannelState.closing)) { return coopSettleChannel([action$, action], channel, deps); } else { return of(channelSettle.failure(new RaidenError(ErrorCodes.CNL_NO_SETTLEABLE_OR_SETTLING_CHANNEL_FOUND, action.meta), action.meta)); } })); } /** * 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 */ export function channelUnlockEpic(action$, state$, deps) { const { log, address, getTokenNetworkContract, config$ } = deps; return action$.pipe(filter(isActionOf(channelSettle.success)), filter((action) => !!(action.payload.confirmed && action.payload.locks?.length)), withLatestFrom(state$), // ensure there's no channel, or if yes, it's a different (by channelId) filter(([action, state]) => state.channels[channelKey(action.meta)]?.id !== action.payload.id), mergeMap(([action]) => { const { tokenNetwork, partner } = action.meta; const locks = concatBytes(action.payload.locks.reduce((acc, lock) => [ ...acc, encode(lock.expiration, 32), encode(lock.amount, 32), lock.secrethash, ], [])); // send unlock transaction return transact(getTokenNetworkContract(tokenNetwork), 'unlock', [action.payload.id, address, partner, locks], deps, { error: ErrorCodes.CNL_ONCHAIN_UNLOCK_FAILED }).pipe(retryWhile(intervalFromConfig(config$), { maxRetries: 3, onErrors: commonAndFailTxErrors, log: log.info, }), ignoreElements(), catchError((error) => { log.error('Error unlocking pending locks on-chain, ignoring', error); return EMPTY; })); })); } //# sourceMappingURL=settle.js.map