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