UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

249 lines 16.8 kB
"use strict"; 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