raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
249 lines • 16.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.withdrawSendExpireMessageEpic = exports.withdrawAutoExpireEpic = exports.withdrawMessageProcessedEpic = exports.withdrawSendTxEpic = exports.withdrawSendRequestMessageEpic = exports.withdrawResolveEpic = exports.initWithdrawMessagesEpic = void 0;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const channels_1 = require("../../channels");
const utils_1 = require("../../channels/utils");
const config_1 = require("../../config");
const messages_1 = require("../../messages");
const actions_1 = require("../../messages/actions");
const actions_2 = require("../../transport/actions");
const utils_2 = require("../../transport/utils");
const actions_3 = require("../../utils/actions");
const error_1 = require("../../utils/error");
const ethers_1 = require("../../utils/ethers");
const lru_1 = require("../../utils/lru");
const rx_1 = require("../../utils/rx");
const types_1 = require("../../utils/types");
const actions_4 = require("../actions");
const state_1 = require("../state");
const utils_3 = require("./utils");
/**
* Emits withdraw action once for each own non-confirmed message at startup
*
* @param state$ - Observable of RaidenStates
* @returns Observable of withdrawMessage.request|withdrawExpire.success actions
*/
function initWithdrawMessagesEpic({}, state$) {
return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(function* (state) {
for (const channel of Object.values(state.channels)) {
if (channel.state !== channels_1.ChannelState.open)
continue;
for (const message of channel.own.pendingWithdraws) {
if (message.type === messages_1.MessageType.WITHDRAW_REQUEST &&
!channel.own.pendingWithdraws.some((0, utils_3.matchWithdraw)(messages_1.MessageType.WITHDRAW_CONFIRMATION, message)))
yield actions_4.withdrawMessage.request({ message }, {
direction: state_1.Direction.SENT,
tokenNetwork: channel.tokenNetwork,
partner: channel.partner.address,
totalWithdraw: message.total_withdraw,
expiration: message.expiration.toNumber(),
});
else if (message.type === messages_1.MessageType.WITHDRAW_EXPIRED)
yield actions_4.withdrawExpire.success({ message }, {
direction: state_1.Direction.SENT,
tokenNetwork: channel.tokenNetwork,
partner: channel.partner.address,
totalWithdraw: message.total_withdraw,
expiration: message.expiration.toNumber(),
});
// WithdrawConfirmations are sent only as response of requests
}
}
}));
}
exports.initWithdrawMessagesEpic = initWithdrawMessagesEpic;
/**
* Resolve a withdrawResolve action and emit withdraw.request
* Resolving withdraws require that partner is online and contracts support `cooperativeSettle`.
*
* @param action$ - Observable of withdrawResolve actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.getTokenNetworkContract - TokenNetwork contract getter
* @returns Observable of withdraw.request|withdraw.failure actions
*/
function withdrawResolveEpic(action$, {}, { getTokenNetworkContract }) {
return action$.pipe((0, rx_1.dispatchRequestAndGetResponse)(actions_2.matrixPresence, (requestPresence$) => action$.pipe((0, operators_1.filter)(actions_4.withdrawResolve.is), (0, operators_1.mergeMap)((action) => {
let preCheck$ = (0, rxjs_1.of)(true);
if (action.payload?.coopSettle) {
const tokenNetworkContract = getTokenNetworkContract(action.meta.tokenNetwork);
preCheck$ = (0, ethers_1.checkContractHasMethod$)(tokenNetworkContract, 'cooperativeSettle');
}
return preCheck$.pipe((0, operators_1.mergeMapTo)(requestPresence$(actions_2.matrixPresence.request(undefined, { address: action.meta.partner }))), (0, operators_1.map)((presence) => {
// assert shouldn't fail, because presence request would, but just in case
(0, error_1.assert)(presence.payload.available, 'partner offline');
return actions_4.withdraw.request(action.payload, action.meta);
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdraw.failure(err, action.meta))));
}))));
}
exports.withdrawResolveEpic = withdrawResolveEpic;
/**
* Retry sending 'WithdrawRequest' messages to partner until WithdrawConfirmation is received
*
* @param action$ - Observable of withdrawRequest.request actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.config$ - Config observable
* @returns Observable of messageSend.request actions
*/
function withdrawSendRequestMessageEpic(action$, state$, { config$ }) {
return action$.pipe((0, operators_1.filter)(actions_4.withdrawMessage.request.is), (0, operators_1.filter)((action) => action.meta.direction === state_1.Direction.SENT), (0, operators_1.mergeMap)((action) => {
const message = action.payload.message;
const send = actions_1.messageSend.request({ message }, { address: action.meta.partner, msgId: message.message_identifier.toString() });
// emits to stop retry loop when channel isn't open anymore, a confirmation came or request
// got cleared from state (e.g. by effective withdraw tx)
const notifier = state$.pipe((0, operators_1.filter)((state) => {
const channel = state.channels[(0, utils_1.channelKey)(action.meta)];
return (channel?.state !== channels_1.ChannelState.open ||
channel.own.pendingWithdraws.some((0, utils_3.matchWithdraw)(messages_1.MessageType.WITHDRAW_CONFIRMATION, action.meta)) ||
!channel.own.pendingWithdraws.some((0, utils_3.matchWithdraw)(messages_1.MessageType.WITHDRAW_REQUEST, action.meta)));
}));
// emit request once immediatelly, then wait until success, then retry every 30s
return (0, utils_3.retrySendUntil$)(send, action$, notifier, (0, config_1.intervalFromConfig)(config$));
}));
}
exports.withdrawSendRequestMessageEpic = withdrawSendRequestMessageEpic;
/**
* Upon valid [[WithdrawConfirmation]], send the on-chain withdraw transaction
*
* @param action$ - Observable of withdrawMessage.success actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @returns Observable of withdraw.success|withdraw.failure actions
*/
function withdrawSendTxEpic(action$, {}, deps) {
const { address, log, getTokenNetworkContract, latest$, config$ } = deps;
return action$.pipe((0, operators_1.filter)(actions_4.withdrawMessage.success.is), (0, operators_1.filter)((action) => action.meta.direction === state_1.Direction.SENT), (0, operators_1.groupBy)((action) => (0, utils_1.channelKey)(action.meta)), (0, operators_1.mergeMap)((grouped$) => grouped$.pipe(
// concatMap handles only one withdraw tx per channel at a time
(0, operators_1.concatMap)((action) => (0, rxjs_1.combineLatest)([latest$, config$]).pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(([{ state }, { revealTimeout }]) => {
const channel = state.channels[grouped$.key];
(0, error_1.assert)(channel?.state === channels_1.ChannelState.open, 'channel not open');
const req = channel.own.pendingWithdraws.find((0, utils_3.matchWithdraw)(messages_1.MessageType.WITHDRAW_REQUEST, action.payload.message));
(0, error_1.assert)(req, 'no matching WithdrawRequest found');
// don't send withdraw tx if this is a coop_settle request (back or forth)
if ('coop_settle' in req)
return rxjs_1.EMPTY;
(0, error_1.assert)(action.meta.totalWithdraw.gt(channel.own.withdraw), 'withdraw already performed');
// don't send on-chain tx if we're 'revealTimeout' seconds from expiration
// this is our confidence threshold when we can get a tx inside timeout
(0, error_1.assert)(req.expiration.gte(Math.floor(Date.now() / 1e3 + revealTimeout)), error_1.ErrorCodes.CNL_WITHDRAW_EXPIRES_SOON);
const { tokenNetwork } = action.meta;
return (0, utils_1.transact)(getTokenNetworkContract(tokenNetwork), 'setTotalWithdraw', [
channel.id,
address,
action.meta.totalWithdraw,
action.meta.expiration,
req.signature,
action.payload.message.signature,
], deps, { error: error_1.ErrorCodes.CNL_WITHDRAW_TRANSACTION_FAILED }).pipe((0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), {
maxRetries: 5,
onErrors: error_1.commonTxErrors,
log: log.info,
}), (0, operators_1.mergeMap)(([, receipt]) => action$.pipe((0, operators_1.filter)((0, actions_3.isConfirmationResponseOf)(actions_4.withdraw, action.meta)), (0, operators_1.take)(1), (0, operators_1.ignoreElements)(),
// startWith unconfirmed success, but complete only on confirmation/failure
(0, operators_1.startWith)(actions_4.withdraw.success({
txHash: receipt.transactionHash,
txBlock: receipt.blockNumber,
// no sensitive value in payload, let confirmationEpic confirm it
confirmed: undefined,
}, action.meta)))), (0, operators_1.startWith)((0, actions_4.withdrawBusy)(undefined, action.meta)));
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdraw.failure(err, action.meta))))))));
}
exports.withdrawSendTxEpic = withdrawSendTxEpic;
/**
* Upon receiving withdraw request or confirmation, send partner the respective Processed message
*
* SDK-based clients (with caps.Delivery set) don't need it, so skip. They, instead, confirm
* the request with the confirmation, and re-sends confirmation on request retries
*
* @param action$ - Observable of withdrawMessage.request|withdrawMessage.success actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.signer - Signer instance
* @param deps.log - Logger instance
* @returns Observable of messageSend.request actions
*/
function withdrawMessageProcessedEpic(action$, {}, { signer, log }) {
const cache = new lru_1.LruCache(32);
return action$.pipe((0, operators_1.filter)((0, actions_3.isActionOf)([actions_4.withdrawMessage.request, actions_4.withdrawMessage.success])), (0, operators_1.filter)((action) => (action.meta.direction === state_1.Direction.RECEIVED) !== actions_4.withdrawMessage.success.is(action)), (0, operators_1.withLatestFrom)(action$.pipe((0, utils_2.getPresencesByAddress)())), (0, operators_1.concatMap)(([action, presences]) => (0, rxjs_1.defer)(() => {
// light-clients don't need Processed messages, only PCs
if ((0, utils_2.peerIsOnlineLC)(presences[action.meta.partner]))
return rxjs_1.EMPTY;
const message = action.payload.message;
let processed$;
const cacheKey = message.message_identifier.toString();
const cached = cache.get(cacheKey);
if (cached)
processed$ = (0, rxjs_1.of)(cached);
else {
const processed = {
type: messages_1.MessageType.PROCESSED,
message_identifier: message.message_identifier,
};
processed$ = (0, rxjs_1.from)((0, messages_1.signMessage)(signer, processed, { log })).pipe((0, operators_1.tap)((signed) => cache.set(cacheKey, signed)));
}
return processed$.pipe((0, operators_1.map)((processed) => actions_1.messageSend.request({ message: processed }, { address: action.meta.partner, msgId: processed.message_identifier.toString() })));
}).pipe((0, operators_1.catchError)((err) => (log.info('Signing Processed message for Withdraw message failed, ignoring', err), rxjs_1.EMPTY)))));
}
exports.withdrawMessageProcessedEpic = withdrawMessageProcessedEpic;
function withdrawKey(meta) {
return `${(0, utils_1.channelKey)(meta)}|${meta.expiration}|${meta.totalWithdraw.toString()}`;
}
/**
* Dispatch withdrawExpire.request when one of our sent WithdrawRequests expired
*
* @param action$ - Observable of newBlock actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.config$ - Config observable
* @param deps.latest$ - Latest observable
* @returns Observable of withdrawExpired actions
*/
function withdrawAutoExpireEpic(action$, {}, { config$, latest$ }) {
const busyWithdraws = new Set();
return action$.pipe((0, operators_1.filter)(actions_4.withdrawMessage.request.is), (0, operators_1.filter)(({ meta }) => meta.direction === state_1.Direction.SENT), (0, operators_1.distinct)(({ payload }) => payload.message.message_identifier.toHexString()), (0, operators_1.withLatestFrom)(config$, action$.pipe((0, operators_1.filter)((0, actions_3.isActionOf)([actions_4.withdrawBusy, actions_4.withdraw.success, actions_4.withdraw.failure])), (0, operators_1.scan)((acc, action) => {
// a string representing uniquely this withdraw.meta
const key = withdrawKey(action.meta);
// 'busy' adds withdraw to set, preventing these withdraws from failing due to expiration
// too soon; withdraw.success|failure clears it
if (actions_4.withdrawBusy.is(action))
acc.add(key);
else
acc.delete(key);
return acc;
}, busyWithdraws), (0, operators_1.startWith)(busyWithdraws))), (0, operators_1.mergeMap)(([action, { httpTimeout, revealTimeout }, busyWithdraws]) => {
const key = withdrawKey(action.meta);
return (0, rxjs_1.merge)((0, rxjs_1.timer)(new Date((action.meta.expiration - revealTimeout) * 1e3), httpTimeout).pipe((0, operators_1.filter)(() => !busyWithdraws.has(key)), (0, operators_1.take)(1), (0, operators_1.mapTo)(actions_4.withdraw.failure(new error_1.RaidenError(error_1.ErrorCodes.CNL_WITHDRAW_EXPIRED), action.meta))), (0, rxjs_1.timer)(new Date(action.meta.expiration * 1e3 + httpTimeout), httpTimeout).pipe((0, operators_1.filter)(() => !busyWithdraws.has(withdrawKey(action.meta))), (0, operators_1.mapTo)(actions_4.withdrawExpire.request(undefined, action.meta)))).pipe((0, operators_1.takeUntil)(latest$.pipe((0, operators_1.filter)(({ state }) => {
const channel = state.channels[(0, utils_1.channelKey)(action.meta)];
return (channel?.state !== channels_1.ChannelState.open ||
channel.own.pendingWithdraws.some((0, utils_3.matchWithdraw)(messages_1.MessageType.WITHDRAW_EXPIRED, action.meta)));
}),
// give up expireRequest & failed timers in case of success
(0, operators_1.mergeWith)(action$.pipe((0, operators_1.filter)(actions_4.withdraw.success.is), (0, operators_1.filter)((a) => withdrawKey(a.meta) === key))))));
}));
}
exports.withdrawAutoExpireEpic = withdrawAutoExpireEpic;
/**
* Retry sending own WithdrawExpired messages until Processed, completes withdraw then
*
* @param action$ - Observable of withdrawExpire.success actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.config$ - Config observable
* @returns Observable of messageSend.request actions
*/
function withdrawSendExpireMessageEpic(action$, {}, { config$ }) {
return action$.pipe((0, operators_1.filter)(actions_4.withdrawExpire.success.is), (0, operators_1.filter)((action) => action.meta.direction === state_1.Direction.SENT), (0, operators_1.mergeMap)((action) => {
const message = action.payload.message;
const send = actions_1.messageSend.request({ message }, { address: action.meta.partner, msgId: message.message_identifier.toString() });
// emits to stop retry when a Processed for this WithdrawExpired comes
const notifier = action$.pipe((0, operators_1.filter)((0, messages_1.isMessageReceivedOfType)((0, types_1.Signed)(messages_1.Processed))), (0, operators_1.filter)((a) => a.meta.address === action.meta.partner &&
a.payload.message.message_identifier.eq(message.message_identifier)), (0, operators_1.take)(1), (0, operators_1.mapTo)((0, actions_4.withdrawCompleted)(undefined, action.meta)), (0, operators_1.share)());
// besides using notifier to stop retry, also merge the withdrawCompleted output action
return (0, rxjs_1.merge)((0, utils_3.retrySendUntil$)(send, action$, notifier, (0, config_1.intervalFromConfig)(config$)), notifier);
}));
}
exports.withdrawSendExpireMessageEpic = withdrawSendExpireMessageEpic;
//# sourceMappingURL=withdraw.js.map