raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
106 lines • 6.42 kB
JavaScript
import pick from 'lodash/pick';
import { combineLatest, EMPTY, of } from 'rxjs';
import { catchError, concatMap, filter, first, groupBy, map, mergeMap, startWith, withLatestFrom, } from 'rxjs/operators';
import { ChannelState } from '../../channels';
import { channelSettle } from '../../channels/actions';
import { channelAmounts, channelKey } from '../../channels/utils';
import { MessageType } from '../../messages';
import { assert } from '../../utils/error';
import { checkContractHasMethod$ } from '../../utils/ethers';
import { dispatchRequestAndGetResponse } from '../../utils/rx';
import { withdraw, withdrawBusy, withdrawMessage } from '../actions';
import { Direction } from '../state';
import { matchWithdraw, withdrawMetaFromRequest } from './utils';
/**
* Upon valid [[WithdrawConfirmation]] for a [[WithdrawRequest]].coop_settle=true from partner,
* also send a [[WithdrawRequest]] with whole balance
*
* @param action$ - Observable of withdrawMessage.success actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.log - Logger instance
* @param deps.getTokenNetworkContract - TokenNetwork contract getter
* @returns Observable of withdraw.request(coop_settle=false) actions
*/
export function coopSettleWithdrawReplyEpic(action$, state$, { log, getTokenNetworkContract }) {
return action$.pipe(filter(withdrawMessage.success.is), filter((action) => action.meta.direction === Direction.RECEIVED), withLatestFrom(state$), mergeMap(([action, state]) => {
const tokenNetworkContract = getTokenNetworkContract(action.meta.tokenNetwork);
return checkContractHasMethod$(tokenNetworkContract, 'cooperativeSettle').pipe(mergeMap(() => {
const channel = state.channels[channelKey(action.meta)];
assert(channel?.state === ChannelState.open, 'channel not open');
const req = channel.partner.pendingWithdraws.find(matchWithdraw(MessageType.WITHDRAW_REQUEST, action.payload.message));
assert(req, 'no matching WithdrawRequest found'); // shouldn't happen
// only reply if this is a coop settle request from partner
if (!req.coop_settle)
return EMPTY;
const { ownTotalWithdrawable } = channelAmounts(channel);
return of(withdraw.request({ coopSettle: false }, {
...action.meta,
direction: Direction.SENT,
totalWithdraw: ownTotalWithdrawable,
}));
}), catchError((error) => {
log.warn('Could not reply to CoopSettle request, ignoring', { action, error });
return EMPTY;
}));
}));
}
/**
* When both valid [[WithdrawConfirmation]] for a [[WithdrawRequest]].coop_settle=true from us,
* send a channelSettle.request
*
* @param action$ - Observable of withdrawMessage.success actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.latest$ - Latest observable
* @param deps.config$ - Config observable
* @param deps.log - Logger instance
* @returns Observable of channelSettle.request|withdraw.failure|success|withdrawBusy actions
*/
export function coopSettleEpic(action$, {}, { latest$, config$, log }) {
return action$.pipe(dispatchRequestAndGetResponse(channelSettle, (requestSettle$) => action$.pipe(filter(withdrawMessage.success.is), groupBy((action) => channelKey(action.meta)), mergeMap((grouped$) => grouped$.pipe(concatMap((action) =>
// observable inside concatMap ensures the body is evaluated at subscription time
combineLatest([latest$, config$]).pipe(first(), mergeMap(([{ state }, { revealTimeout }]) => {
const channel = state.channels[channelKey(action.meta)];
assert(channel?.state === ChannelState.open, 'channel not open');
const { ownCapacity, partnerCapacity, ownTotalWithdrawable, partnerTotalWithdrawable, } = channelAmounts(channel);
// when both capacities are zero, both sides should be ready; before that, just
// skip silently, a matching state may come later or withdraw will expire
assert((!channel.own.locks.length &&
!channel.partner.locks.length &&
ownCapacity.isZero()) ||
partnerCapacity.isZero(), '');
const ownReq = channel.own.pendingWithdraws.find((msg) => msg.type === MessageType.WITHDRAW_REQUEST &&
msg.expiration.gte(Math.floor(Date.now() / 1e3 + revealTimeout)) &&
msg.total_withdraw.eq(ownTotalWithdrawable) &&
!!msg.coop_settle);
// not our request or expires too soon
assert(ownReq, 'own request not found');
const ownConfirmation = channel.own.pendingWithdraws.find(matchWithdraw(MessageType.WITHDRAW_CONFIRMATION, ownReq));
const partnerReq = channel.partner.pendingWithdraws.find((msg) => msg.type === MessageType.WITHDRAW_REQUEST &&
msg.expiration.gte(Math.floor(Date.now() / 1e3 + revealTimeout)) &&
msg.total_withdraw.eq(partnerTotalWithdrawable));
assert(partnerReq, 'partner request not found'); // shouldn't happen
const partnerConfirmation = channel.partner.pendingWithdraws.find(matchWithdraw(MessageType.WITHDRAW_CONFIRMATION, partnerReq));
assert(ownConfirmation && partnerConfirmation, [
'no matching WithdrawConfirmations found',
{ ownConfirmation, partnerConfirmation },
]);
const withdrawMeta = withdrawMetaFromRequest(ownReq, channel);
return requestSettle$(channelSettle.request({
coopSettle: [
[ownReq, ownConfirmation],
[partnerReq, partnerConfirmation],
],
}, { tokenNetwork: withdrawMeta.tokenNetwork, partner: withdrawMeta.partner })).pipe(map((success) => withdraw.success(pick(success.payload, ['txBlock', 'txHash', 'confirmed']), withdrawMeta)), catchError((err) => of(withdraw.failure(err, withdrawMeta))),
// prevents this withdraw from expire-failing while we're trying to settle
startWith(withdrawBusy(undefined, withdrawMeta)));
}), catchError((err) => {
if (err.message)
log.info(err.message, err.details);
// these errors are just the asserts, to be ignored;
// []actual errors are only the catched inside requestSettle$'s pipe
return EMPTY;
}))))))));
}
//# sourceMappingURL=coopsettle.js.map