UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

865 lines 48.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.transferGenerateAndSignEnvelopeMessageEpic = void 0; const bignumber_1 = require("@ethersproject/bignumber"); const constants_1 = require("@ethersproject/constants"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const state_1 = require("../../channels/state"); const utils_1 = require("../../channels/utils"); const constants_2 = require("../../constants"); const actions_1 = require("../../messages/actions"); const types_1 = require("../../messages/types"); const utils_2 = require("../../messages/utils"); const actions_2 = require("../../transport/actions"); const utils_3 = require("../../transport/utils"); const utils_4 = require("../../utils"); const actions_3 = require("../../utils/actions"); const error_1 = require("../../utils/error"); const lru_1 = require("../../utils/lru"); const rx_1 = require("../../utils/rx"); const types_2 = require("../../utils/types"); const actions_4 = require("../actions"); const state_2 = require("../state"); const utils_5 = require("../utils"); const utils_6 = require("./utils"); // calculate locks array for channel end without lock with given secrethash function withoutLock(end, secrethash) { const locks = end.locks.filter((l) => l.secrethash !== secrethash); (0, utils_4.assert)(locks.length === end.locks.length - 1, 'invalid locks size'); return locks; } // calculate locked amount for a given locks array function totalLocked(locks) { return locks.reduce((acc, { amount }) => acc.add(amount), constants_1.Zero); } // gets and asserts channel is open and optionally matches chain_id and channel_identifier function getOpenChannel(state, key, matches) { const channel = state.channels[(0, utils_1.channelKey)(key)]; (0, utils_4.assert)(channel?.state === state_1.ChannelState.open, 'channel not open'); if (matches) { (0, utils_4.assert)(matches.chain_id.eq(state.chainId), 'chainId mismatch'); (0, utils_4.assert)(matches.channel_identifier.eq(channel.id), 'channelId mismatch'); } return channel; } function transferRequestIsResolved(action) { return action.payload.resolved; } /** * The core logic of {@link makeAndSignTransfer}. * * @param state - Contains The current state of the app * @param action - transfer request action to be sent. * @param latest - Latest object * @param latest.config - Config object * @param latest.settleTimeout - contract's settleTimeout * @param deps - {@link RaidenEpicDeps} * @param deps.log - Logger instance * @param deps.address - Our address * @param deps.network - Current Network * @param deps.signer - Signer instance * @returns Observable of {@link transferSecret} or {@link transferSigned} actions */ function makeAndSignTransfer$(state, action, { config, settleTimeout }, { log, address, network, signer }) { const { revealTimeout, expiryFactor } = config; const { tokenNetwork, fee, partner, userId } = action.payload; const channel = getOpenChannel(state, { tokenNetwork, partner }); const now = Math.round(Date.now() / 1e3); // now() in seconds (for messages and contracts) let expiration; if ('expiration' in action.payload && action.payload.expiration) { expiration = action.payload.expiration; // reuse passed as is, in seconds } else { expiration = now; if ('lockTimeout' in action.payload && action.payload.lockTimeout) expiration += action.payload.lockTimeout; else expiration += Math.min(revealTimeout * expiryFactor, settleTimeout - 1); } expiration = (0, types_2.decode)((0, types_2.UInt)(32), Math.ceil(expiration)); // revealTimeout < Δexpiration < settleTimeout // to ensure we'll have enough time to tx in case it expires, but it can't outlive channel (0, utils_4.assert)(expiration.gt(now + revealTimeout), [ 'expiration too soon', { expiration, blockNumber: state.blockNumber, settleTimeout, revealTimeout }, ]); (0, utils_4.assert)(expiration.lt(now + settleTimeout), [ 'expiration too far in the future', { expiration, blockNumber: state.blockNumber, settleTimeout, revealTimeout }, ]); const lock = { // fee is added to the lock amount; overflow is checked on locksSum below amount: action.payload.value.add(fee), expiration, secrethash: action.meta.secrethash, }; const locks = [...channel.own.locks, lock]; const locksSum = totalLocked(locks); (0, utils_4.assert)((0, types_2.UInt)(32).is(channel.own.balanceProof.transferredAmount.add(locksSum)), 'overflow on future transferredAmount'); const locksroot = (0, utils_5.getLocksroot)(locks); log.info('Signing transfer of value', action.payload.value.toString(), 'of token', channel.token, ', to', action.payload.target, ', through routes', action.payload.metadata, ', paying', fee.toString(), 'in fees.'); const message = { type: types_1.MessageType.LOCKED_TRANSFER, message_identifier: (0, utils_5.makeMessageId)(), chain_id: bignumber_1.BigNumber.from(network.chainId), token_network_address: action.payload.tokenNetwork, channel_identifier: bignumber_1.BigNumber.from(channel.id), nonce: channel.own.nextNonce, transferred_amount: channel.own.balanceProof.transferredAmount, locked_amount: locksSum, locksroot, payment_identifier: action.payload.paymentId, token: channel.token, recipient: partner, lock, target: action.payload.target, initiator: action.payload.initiator ?? address, metadata: action.payload.metadata, // passthrough unchanged metadata }; return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log })).pipe((0, operators_1.mergeMap)(function* (signed) { // messageSend LockedTransfer handled by transferRetryMessageEpic yield (0, actions_4.transferSigned)({ message: signed, fee, partner, userId }, action.meta); // besides transferSigned, also yield transferSecret (for registering) if we know it if (action.payload.secret) yield (0, actions_4.transferSecret)({ secret: action.payload.secret }, action.meta); })); } /** * Create an observable to compose and sign a LockedTransfer message/transferSigned action * As it's an async observable which depends on state and may return an action which changes it, * the returned observable must be subscribed in a serialized context that ensures non-concurrent * write access to the channel's balance proof (e.g. concatMap) * * @param state$ - Observable of current state * @param action - transfer request action to be sent * @param deps - RaidenEpicDeps * @returns Observable of transferSigned|transferSecret|transfer.failure actions */ function sendTransferSigned(state$, action, deps) { return (0, rxjs_1.combineLatest)([state$, deps.latest$]).pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(([state, latest]) => { if (deps.db.storageKeys.has((0, utils_5.transferKey)(action.meta))) { // don't throw to avoid emitting transfer.failure, to just wait for already pending transfer deps.log.warn('transfer already present', action.meta); return rxjs_1.EMPTY; } return makeAndSignTransfer$(state, action, latest, deps); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.transfer.failure(err, action.meta)))); } /** * Contains the core logic of {@link makeAndSignUnlock}. * * @param state$ - Observable of the latest app state. * @param state - Contains The current state of the app * @param action - The transfer unlock action that will generate the transferUnlock.success action. * @param transferState - Transfer's state * @param deps - Epics dependencies * @param deps.signer - The signer that will sign the message * @param deps.log - Logger instance * @param deps.db - Database instance * @returns Observable of {@link transferUnlock.success} action. */ function makeAndSignUnlock$(state$, state, action, transferState, { log, signer, db }) { const secrethash = action.meta.secrethash; const locked = transferState.transfer; const tokenNetwork = locked.token_network_address; const partner = transferState.partner; const channel = getOpenChannel(state, { tokenNetwork, partner }); let signed$; if (transferState.unlock) { // unlock already signed, use cached signed$ = (0, rxjs_1.of)(transferState.unlock); } else { (0, utils_4.assert)(transferState.secret, 'unknown secret'); // never fails because we wait before (0, utils_4.assert)(channel.own.locks.find((lock) => lock.secrethash === secrethash), 'transfer already unlocked or expired'); // don't forget to check after signature too, may have expired by then // allow unlocking past expiration if secret registered on-chain (0, utils_4.assert)(transferState.secretRegistered || transferState.expiration >= Date.now() / 1e3, 'lock expired'); const message = { type: types_1.MessageType.UNLOCK, message_identifier: (0, utils_5.makeMessageId)(), chain_id: locked.chain_id, token_network_address: locked.token_network_address, channel_identifier: locked.channel_identifier, nonce: channel.own.nextNonce, transferred_amount: (0, types_2.decode)((0, types_2.UInt)(32), channel.own.balanceProof.transferredAmount.add(locked.lock.amount), 'overflow on transferredAmount'), locked_amount: channel.own.balanceProof.lockedAmount.sub(locked.lock.amount), locksroot: (0, utils_5.getLocksroot)(withoutLock(channel.own, secrethash)), payment_identifier: locked.payment_identifier, secret: transferState.secret, }; signed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log })); } return signed$.pipe((0, rx_1.withMergeFrom)(async () => (0, utils_5.getTransfer)(state$, db, action.meta)), (0, operators_1.withLatestFrom)(state$), (0, operators_1.map)(([[signed, transferState], state]) => { const channel = getOpenChannel(state, { tokenNetwork, partner }); (0, utils_4.assert)(transferState && (transferState.expiration > Date.now() / 1e3 || transferState.secretRegistered || channel.own.locks.find((lock) => lock.secrethash === secrethash && lock.registered)), 'lock expired'); return actions_4.transferUnlock.success({ message: signed, partner, userId: action.payload?.userId }, action.meta); // messageSend Unlock handled by transferRetryMessageEpic // we don't check if transfer was refunded. If partner refunded the transfer but still // forwarded the payment, we still act honestly and unlock if they revealed })); } /** * Create an observable to compose and sign a Unlock message/transferUnlock.success action * As it's an async observable which depends on state and may return an action which changes it, * the returned observable must be subscribed in a serialized context that ensures non-concurrent * write access to the channel's balance proof (e.g. concatMap) * * @param state$ - Observable of current state * @param action - transferUnlock.request request action to be sent * @param deps - RaidenEpicDeps members * @param deps.signer - Signer instance * @param deps.log - Logger instance * @param deps.db - Database instance * @returns Observable of transferUnlock.success actions */ function sendTransferUnlocked(state$, action, deps) { const { log, db } = deps; return (0, rxjs_1.defer)(async () => (0, utils_5.getTransfer)(state$, db, action.meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => makeAndSignUnlock$(state$, state, action, transferState, deps)), (0, operators_1.catchError)((err) => { log.warn('Error trying to unlock after SecretReveal', err); return (0, rxjs_1.of)(actions_4.transferUnlock.failure(err, action.meta)); })); } /** * Contains the core logic of {@link makeAndSignLockExpired}. * * @param state - Contains The current state of the app * @param action - The transfer expire action. * @param transferState - State of the transfer * @param deps - RaidenEpicDeps members * @param deps.signer - Signer instance * @param deps.log - Logger instance * @returns Observable of transferExpire.success actions */ function makeAndSignLockExpired$(state, action, transferState, { signer, log }) { const secrethash = action.meta.secrethash; const locked = transferState.transfer; const tokenNetwork = locked.token_network_address; const partner = transferState.partner; const channel = getOpenChannel(state, { tokenNetwork, partner }); let signed$; if (transferState.expired) { // expired already signed, use cached signed$ = (0, rxjs_1.of)(transferState.expired); } else { (0, utils_4.assert)(locked.lock.expiration.lt(Math.floor(Date.now() / 1e3)), 'lock not yet expired'); (0, utils_4.assert)(channel.own.locks.find((lock) => lock.secrethash === secrethash) && !transferState.unlock, 'transfer already unlocked or expired'); const locksroot = (0, utils_5.getLocksroot)(withoutLock(channel.own, secrethash)); const message = { type: types_1.MessageType.LOCK_EXPIRED, message_identifier: (0, utils_5.makeMessageId)(), chain_id: locked.chain_id, token_network_address: locked.token_network_address, channel_identifier: locked.channel_identifier, nonce: channel.own.nextNonce, transferred_amount: channel.own.balanceProof.transferredAmount, locked_amount: channel.own.balanceProof.lockedAmount.sub(locked.lock.amount), locksroot, recipient: partner, secrethash, }; signed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log })); } return signed$.pipe( // messageSend LockExpired handled by transferRetryMessageEpic (0, operators_1.map)((signed) => actions_4.transferExpire.success({ message: signed, partner }, action.meta))); } /** * Create an observable to compose and sign a LockExpired message/transferExpire.success action * As it's an async observable which depends on state and may return an action which changes it, * the returned observable must be subscribed in a serialized context that ensures non-concurrent * write access to the channel's balance proof (e.g. concatMap) * * @param state$ - Observable of current state * @param action - transfer request action to be sent * @param deps - RaidenEpicDeps members * @param deps.log - Logger instance * @param deps.signer - Signer instance * @param deps.db - Database instance * @returns Observable of transferExpire.success|transferExpire.failure actions */ function sendTransferExpired(state$, action, { log, signer, db }) { return (0, rxjs_1.defer)(() => (0, utils_5.getTransfer)(state$, db, action.meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => makeAndSignLockExpired$(state, action, transferState, { signer, log })), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.transferExpire.failure(err, action.meta)))); } function receiveTransferSigned(state$, action, { address, log, signer, config$, db, latest$ }) { const secrethash = action.payload.message.lock.secrethash; const meta = { secrethash, direction: state_2.Direction.RECEIVED }; return (0, rxjs_1.combineLatest)([state$, config$, latest$]).pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(([state, { revealTimeout, caps }, { settleTimeout }]) => { const locked = action.payload.message; if (db.storageKeys.has((0, utils_5.transferKey)(meta))) { log.warn('transfer already present', meta); const msgId = locked.message_identifier; const transferState = state.transfers[(0, utils_5.transferKey)(meta)]; // if transfer matches the stored one, re-send Processed once if (transferState?.transferProcessed && transferState.partner === action.meta.address && msgId.eq(transferState.transfer.message_identifier)) { // transferProcessed again will trigger messageSend.request return (0, rxjs_1.of)((0, actions_4.transferProcessed)({ message: (0, types_2.untime)(transferState.transferProcessed), userId: action.payload.userId }, meta)); } return rxjs_1.EMPTY; } // full balance proof validation const tokenNetwork = locked.token_network_address; const partner = action.meta.address; const channel = getOpenChannel(state, { tokenNetwork, partner }, locked); (0, utils_4.assert)(locked.nonce.eq(channel.partner.nextNonce), [ 'nonce mismatch', { expected: channel.partner.nextNonce.toNumber(), received: locked.nonce.toNumber() }, ]); const locks = [...channel.partner.locks, locked.lock]; const locksroot = (0, utils_5.getLocksroot)(locks); (0, utils_4.assert)(locked.locksroot === locksroot, 'locksroot mismatch'); (0, utils_4.assert)(locked.transferred_amount.eq(channel.partner.balanceProof.transferredAmount), 'transferredAmount mismatch'); const locksSum = totalLocked(locks); (0, utils_4.assert)(locked.locked_amount.eq(locksSum), 'lockedAmount mismatch'); (0, utils_4.assert)((0, types_2.UInt)(32).is(channel.partner.balanceProof.transferredAmount.add(locksSum)), 'overflow on future transferredAmount'); const { partnerCapacity } = (0, utils_1.channelAmounts)(channel); (0, utils_4.assert)(locked.lock.amount.lte(partnerCapacity), 'balanceProof total amount bigger than capacity'); (0, utils_4.assert)(locked.lock.expiration.lt(Math.ceil(Date.now() / 1e3 + settleTimeout)), [ 'expiration too far in the future', { expiration: locked.lock.expiration, blockNumber: state.blockNumber, settleTimeout, revealTimeout, }, ]); // accept expired transfers, to apply state change and stay in sync // with partner, so we can receive later LockExpired and transfers on top of it (0, utils_4.assert)(locked.recipient === address, "Received transfer isn't for us"); log.info('Receiving transfer of value', locked.lock.amount.toString(), 'of token', channel.token, ', from', locked.initiator, ', through partner', partner); let request$ = (0, rxjs_1.of)(undefined); if (locked.target === address) { let ignoredDetails; // danger zone (our interval of confidence we can get secret register tx on-chain) // starts revealTimeout millis before lock expiration (in secs) const dangerZoneStart = locked.lock.expiration.sub(revealTimeout); if (!(0, utils_3.getCap)(caps, constants_2.Capabilities.RECEIVE)) ignoredDetails = { reason: 'receiving disabled' }; else if (!dangerZoneStart.mul(1e3).gt(Date.now())) ignoredDetails = { reason: 'lock expired or expires too soon', lockExpiration: locked.lock.expiration, dangerZoneStart, revealTimeout, }; if (ignoredDetails) { log.warn('Ignoring received transfer', ignoredDetails); } else { request$ = (0, rxjs_1.defer)(async () => { const decryptedSecret = (0, utils_5.decryptSecretFromMetadata)(locked.metadata, [secrethash, locked.lock.amount, locked.payment_identifier], signer); if (decryptedSecret) return decryptedSecret; const request = { type: types_1.MessageType.SECRET_REQUEST, payment_identifier: locked.payment_identifier, secrethash, amount: locked.lock.amount, expiration: locked.lock.expiration, message_identifier: (0, utils_5.makeMessageId)(), }; return (0, utils_2.signMessage)(signer, request, { log }); }); } } const processed$ = (0, rxjs_1.defer)(async () => { const processed = { type: types_1.MessageType.PROCESSED, message_identifier: locked.message_identifier, }; return (0, utils_2.signMessage)(signer, processed, { log }); }); // if any of these signature prompts fail, none of these actions will be emitted return (0, rxjs_1.combineLatest)([processed$, request$]).pipe((0, operators_1.mergeMap)(function* ([processed, requestOrSecret]) { yield (0, actions_4.transferSigned)({ message: locked, fee: constants_1.Zero, partner }, meta); // sets TransferState.transferProcessed yield (0, actions_4.transferProcessed)({ message: processed, userId: action.payload.userId }, meta); if (types_2.Secret.is(requestOrSecret)) { yield (0, actions_4.transferSecret)({ secret: requestOrSecret }, meta); } else if (requestOrSecret) { // request initiator's presence, to be able to request secret yield actions_2.matrixPresence.request(undefined, { address: locked.initiator }); // request secret iff we're the target and receiving is enabled yield (0, actions_4.transferSecretRequest)({ message: requestOrSecret, ...(0, utils_5.searchValidViaAddress)(locked.metadata, locked.initiator), }, meta); } })); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.transfer.failure(err, meta)))); } function receiveTransferUnlocked(state$, action, { log, signer, db }) { const secrethash = (0, utils_5.getSecrethash)(action.payload.message.secret); const meta = { secrethash, direction: state_2.Direction.RECEIVED }; // db.get will throw if not found, being handled on final catchError return (0, rxjs_1.defer)(() => (0, utils_5.getTransfer)(state$, db, meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => { (0, utils_4.assert)(transferState, 'unknown transfer'); const unlock = action.payload.message; const partner = action.meta.address; (0, utils_4.assert)(partner === transferState.partner, 'wrong partner'); // may race on db.get, but will validate on synchronous channel state reducer (0, utils_4.assert)(!transferState.expired, 'already expired'); if (transferState.unlock) { log.warn('transfer already unlocked', action.meta); // if message matches the stored one, re-send Processed once if (transferState.unlockProcessed && unlock.message_identifier.eq(transferState.unlockProcessed.message_identifier)) { // transferProcessed again will trigger messageSend.request return (0, rxjs_1.of)((0, actions_4.transferUnlockProcessed)({ message: (0, types_2.untime)(transferState.unlockProcessed), userId: action.payload.userId }, meta)); } else return rxjs_1.EMPTY; } const locked = transferState.transfer; (0, utils_4.assert)(unlock.token_network_address === locked.token_network_address, 'wrong tokenNetwork'); // unlock validation const tokenNetwork = unlock.token_network_address; const channel = getOpenChannel(state, { tokenNetwork, partner }, unlock); (0, utils_4.assert)(unlock.nonce.eq(channel.partner.nextNonce), [ 'nonce mismatch', { expected: channel.partner.nextNonce.toNumber(), received: unlock.nonce.toNumber() }, ]); const amount = locked.lock.amount; const locks = withoutLock(channel.partner, secrethash); (0, utils_4.assert)(unlock.locksroot === (0, utils_5.getLocksroot)(locks), 'locksroot mismatch'); (0, utils_4.assert)(unlock.transferred_amount.eq(channel.partner.balanceProof.transferredAmount.add(amount)), 'transferredAmount mismatch'); (0, utils_4.assert)(unlock.locked_amount.eq(totalLocked(locks)), 'lockedAmount mismatch'); const processed = { type: types_1.MessageType.PROCESSED, message_identifier: unlock.message_identifier, }; // if any of these signature prompts fail, none of these actions will be emitted return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, processed, { log })).pipe((0, operators_1.mergeMap)(function* (processed) { // we should already know the secret, but if not, persist again if (!transferState.secret) yield (0, actions_4.transferSecret)({ secret: unlock.secret }, meta); yield actions_4.transferUnlock.success({ message: unlock, partner }, meta); // sets TransferState.transferProcessed yield (0, actions_4.transferUnlockProcessed)({ message: processed, userId: action.payload.userId }, meta); yield actions_4.transfer.success({ balanceProof: (0, utils_2.getBalanceProofFromEnvelopeMessage)(unlock) }, meta); })); }), (0, operators_1.catchError)((err) => { log.warn('Error trying to process received Unlock', err); return (0, rxjs_1.of)(actions_4.transferUnlock.failure(err, meta)); })); } function receiveTransferExpired(state$, action, { log, signer, db }) { const secrethash = action.payload.message.secrethash; const meta = { secrethash, direction: state_2.Direction.RECEIVED }; // db.get will throw if not found, being handled on final catchError return (0, rxjs_1.defer)(() => (0, utils_5.getTransfer)(state$, db, meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => { const expired = action.payload.message; const partner = action.meta.address; (0, utils_4.assert)(partner === transferState.partner, 'wrong partner'); // may race on db.get, but will validate on synchronous channel state reducer (0, utils_4.assert)(!transferState.unlock, 'transfer unlocked'); if (transferState.expired) { log.warn('transfer already expired', action.meta); // if message matches the stored one, re-send Processed once if (transferState.expiredProcessed && expired.message_identifier.eq(transferState.expiredProcessed.message_identifier)) { // transferProcessed again will trigger messageSend.request return (0, rxjs_1.of)((0, actions_4.transferExpireProcessed)({ message: (0, types_2.untime)(transferState.expiredProcessed), userId: action.payload.userId }, meta)); } else return rxjs_1.EMPTY; } const locked = transferState.transfer; // expired validation (0, utils_4.assert)(locked.lock.expiration.lt(Math.ceil(Date.now() / 1e3)), 'not expired yet'); (0, utils_4.assert)(!transferState.secretRegistered, 'secret registered onchain'); (0, utils_4.assert)(expired.token_network_address === locked.token_network_address, 'wrong tokenNetwork'); const tokenNetwork = expired.token_network_address; const channel = getOpenChannel(state, { tokenNetwork, partner }, expired); (0, utils_4.assert)(expired.nonce.eq(channel.partner.nextNonce), [ 'nonce mismatch', { expected: channel.partner.nextNonce.toNumber(), received: expired.nonce.toNumber() }, ]); const locks = withoutLock(channel.partner, secrethash); (0, utils_4.assert)(expired.locksroot === (0, utils_5.getLocksroot)(locks), 'locksroot mismatch'); (0, utils_4.assert)(expired.locked_amount.eq(totalLocked(locks)), 'lockedAmount mismatch'); (0, utils_4.assert)(expired.transferred_amount.eq(channel.partner.balanceProof.transferredAmount), 'transferredAmount mismatch'); const processed = { type: types_1.MessageType.PROCESSED, message_identifier: expired.message_identifier, }; // if any of these signature prompts fail, none of these actions will be emitted return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, processed, { log })).pipe((0, operators_1.mergeMap)(function* (processed) { yield actions_4.transferExpire.success({ message: expired, partner }, meta); // sets TransferState.transferProcessed yield (0, actions_4.transferExpireProcessed)({ message: processed, userId: action.payload.userId }, meta); yield actions_4.transfer.failure(new error_1.RaidenError(error_1.ErrorCodes.XFER_EXPIRED, { block: locked.lock.expiration.toString(), }), meta); })); }), (0, operators_1.catchError)((err) => { log.warn('Error trying to process received LockExpired', err); return (0, rxjs_1.of)(actions_4.transferExpire.failure(err, meta)); })); } function isWithdrawConfirmation(m) { return m.type === types_1.MessageType.WITHDRAW_CONFIRMATION; } /** * Handles a withdraw.request and send a WithdrawRequest to partner * * @param state$ - Observable of current state * @param action - Withdraw request which caused this handling * @param deps - RaidenEpicDeps members * @param deps.address - Our address * @param deps.log - Logger instance * @param deps.signer - Signer instance * @param deps.network - Current Network * @param deps.config$ - Config observable * @returns Observable of withdrawMessage.request|withdraw.failure actions */ function sendWithdrawRequest(state$, action, { log, address, signer, network, config$ }) { if (action.meta.direction !== state_2.Direction.SENT) return rxjs_1.EMPTY; return (0, rxjs_1.combineLatest)([state$, config$]).pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(([state, { revealTimeout }]) => { const channel = getOpenChannel(state, action.meta); if (channel.own.pendingWithdraws.some((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_REQUEST, action.meta))) return rxjs_1.EMPTY; // already requested, skip without failing // next assert prevents parallel requests from being performed, but we special case here: // in case there's an still valid confirmation which failed for any reason, we emit it again // in order to retry sending the respective transaction const oldConfirmation = channel.own.pendingWithdraws.find(isWithdrawConfirmation); if (oldConfirmation) { return [ actions_4.withdrawMessage.success({ message: oldConfirmation }, { direction: action.meta.direction, tokenNetwork: channel.tokenNetwork, partner: channel.partner.address, totalWithdraw: oldConfirmation.total_withdraw, expiration: oldConfirmation.expiration.toNumber(), }), actions_4.withdraw.failure(new error_1.RaidenError(error_1.ErrorCodes.CNL_WITHDRAW_RETRY_CONFIRMATION), action.meta), ]; } // although it'd be possible parallel withdraw requests per protocol, PC doesn't like it and // will get out of sync if we try, so we must prevent it (0, utils_4.assert)(!channel.own.pendingWithdraws.length, [ error_1.ErrorCodes.CNL_WITHDRAW_PENDING, { pendingWithdraws: channel.own.pendingWithdraws }, ]); (0, utils_4.assert)(action.meta.totalWithdraw.lte((0, utils_1.channelAmounts)(channel).ownTotalWithdrawable), error_1.ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_HIGH); if (action.payload?.coopSettle !== undefined) { // coopSettle reply has a more relaxed constraint on expiration (0, utils_4.assert)(action.meta.expiration > Date.now() / 1e3, error_1.ErrorCodes.CNL_WITHDRAW_EXPIRES_SOON); const { ownLocked, partnerLocked, ownTotalWithdrawable, partnerCapacity } = (0, utils_1.channelAmounts)(channel); (0, utils_4.assert)(!channel.own.locks.length && !channel.partner.locks.length && action.meta.totalWithdraw.eq(ownTotalWithdrawable) && (action.payload.coopSettle || partnerCapacity.isZero()), [error_1.ErrorCodes.CNL_COOP_SETTLE_NOT_POSSIBLE, { ownLocked, partnerLocked, partnerCapacity }]); } else { (0, utils_4.assert)(action.meta.totalWithdraw.gt(channel.own.withdraw), error_1.ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_LOW); (0, utils_4.assert)(action.meta.expiration >= Date.now() / 1e3 + revealTimeout, error_1.ErrorCodes.CNL_WITHDRAW_EXPIRES_SOON); } const request = { type: types_1.MessageType.WITHDRAW_REQUEST, message_identifier: (0, utils_5.makeMessageId)(), chain_id: bignumber_1.BigNumber.from(network.chainId), token_network_address: action.meta.tokenNetwork, channel_identifier: bignumber_1.BigNumber.from(channel.id), participant: address, total_withdraw: action.meta.totalWithdraw, nonce: channel.own.nextNonce, expiration: bignumber_1.BigNumber.from(action.meta.expiration), ...(action.payload?.coopSettle !== undefined ? { coop_settle: action.payload.coopSettle } : null), }; return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, request, { log })).pipe((0, operators_1.map)((message) => actions_4.withdrawMessage.request({ message }, action.meta))); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdraw.failure(err, action.meta)))); } /** * Validates a received WithdrawConfirmation message * * @param state$ - Observable of current state * @param action - Withdraw request which caused this handling * @param deps - RaidenEpicDeps members * @param deps.address - Our address * @returns Observable of withdrawMessage.success actions */ function receiveWithdrawConfirmation(state$, action, { address }) { const confirmation = action.payload.message; const tokenNetwork = confirmation.token_network_address; const partner = action.meta.address; let expiration; try { expiration = confirmation.expiration.toNumber(); // expiration could be too big } catch (e) { return rxjs_1.EMPTY; } const withdrawMeta = { direction: state_2.Direction.SENT, tokenNetwork, partner, totalWithdraw: confirmation.total_withdraw, expiration, }; return state$.pipe((0, operators_1.first)(), (0, operators_1.map)((state) => { getOpenChannel(state, { tokenNetwork, partner }, confirmation); (0, utils_4.assert)(confirmation.participant === address, 'participant mismatch'); // don't validate request presence here, to always update nonce, but do on tx send return actions_4.withdrawMessage.success({ message: confirmation }, withdrawMeta); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawMessage.failure(err, withdrawMeta)))); } /** * Handles a withdrawExpire.request, sign and send a WithdrawExpired message to partner * * @param state$ - Observable of current state * @param action - WithdrawExpire request which caused this call * @param deps - RaidenEpicDeps members * @param deps.address - Our address * @param deps.log - Logger instance * @param deps.signer - Signer instance * @returns Observable of withdrawMessage.request|withdraw.failure actions */ function sendWithdrawExpired(state$, action, { log, address, signer }) { if (action.meta.direction !== state_2.Direction.SENT) return rxjs_1.EMPTY; return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)((state) => { const channel = getOpenChannel(state, action.meta); (0, utils_4.assert)(!channel.own.pendingWithdraws.some((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_EXPIRED, action.meta)), 'already expired'); const req = channel.own.pendingWithdraws.find((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_REQUEST, action.meta)); (0, utils_4.assert)(req, 'no matching WithdrawRequest found, maybe already confirmed'); (0, utils_4.assert)(action.meta.expiration < Date.now() / 1e3, 'not expired yet'); const expired = { type: types_1.MessageType.WITHDRAW_EXPIRED, message_identifier: (0, utils_5.makeMessageId)(), chain_id: req.chain_id, token_network_address: req.token_network_address, channel_identifier: req.channel_identifier, participant: address, total_withdraw: action.meta.totalWithdraw, nonce: channel.own.nextNonce, expiration: req.expiration, }; return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, expired, { log })).pipe((0, operators_1.map)((message) => actions_4.withdrawExpire.success({ message }, action.meta))); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawExpire.failure(err, action.meta)))); } /** * Validate a [[WithdrawRequest]], compose and send a [[WithdrawConfirmation]] * * Validate channel exists and is open, and that total_withdraw is less than or equal withdrawable * amount. We need it inside [[transferGenerateAndSignEnvelopeMessageEpic]] concatMap/lock because * we read and change 'nextNonce', even though WithdrawConfirmation doesn't carry a full * balanceProof. If request's nonce is valid but it already expired (old replied message), we still * accept the state change, but don't compose/send the confirmation, and let it expire. * Instead of storing confirmation in state and retrying, we just cache it and send the cached * signed message on each retried request received. * * @param state$ - Observable of current state * @param action - Withdraw request which caused this handling * @param signer - RaidenEpicDeps members * @param signer.signer - Signer instance * @param signer.log - Logger instance * @returns Observable of transferExpire.success|transferExpire.failure actions */ function receiveWithdrawRequest(state$, action, { signer, log }) { const request = action.payload.message; const tokenNetwork = request.token_network_address; const partner = request.participant; let expiration; try { expiration = request.expiration.toNumber(); // expiration could be too big } catch (e) { return rxjs_1.EMPTY; } const withdrawMeta = { direction: state_2.Direction.RECEIVED, tokenNetwork, partner, totalWithdraw: request.total_withdraw, expiration, }; return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)((state) => { (0, utils_4.assert)(request.participant === action.meta.address, 'participant mismatch'); const channel = getOpenChannel(state, { tokenNetwork, partner }, request); let confirmation$; const persistedConfirmation = channel.partner.pendingWithdraws.find((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_CONFIRMATION, request)); if (persistedConfirmation) { confirmation$ = (0, rxjs_1.of)(persistedConfirmation); } else { (0, utils_4.assert)(request.nonce.eq(channel.partner.nextNonce), [ 'nonce mismatch', { expected: channel.partner.nextNonce.toNumber(), received: request.nonce.toNumber() }, ]); (0, utils_4.assert)(request.total_withdraw.lte((0, utils_1.channelAmounts)(channel).partnerTotalWithdrawable), 'invalid total_withdraw'); // don't mind expiration, and always reply with confirmation // expired confirmations are useless on-chain, but confirms message const confirmation = { type: types_1.MessageType.WITHDRAW_CONFIRMATION, message_identifier: request.message_identifier, chain_id: request.chain_id, token_network_address: request.token_network_address, channel_identifier: request.channel_identifier, participant: request.participant, total_withdraw: request.total_withdraw, nonce: channel.own.nextNonce, expiration: request.expiration, }; confirmation$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, confirmation, { log })); } return confirmation$.pipe((0, operators_1.mergeMap)(function* (message) { // first, emit 'WithdrawRequest', to increase partner's nonce in state yield actions_4.withdrawMessage.request({ message: request }, withdrawMeta); // emit our composed 'WithdrawConfirmation' to increase our nonce in state yield actions_4.withdrawMessage.success({ message }, withdrawMeta); // send once per received request; confirmation signature is cached above yield actions_1.messageSend.request({ message }, { address: partner, msgId: action.payload.message.message_identifier.toString(), }); })); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawMessage.failure(err, withdrawMeta)))); } /** * Validate a received [[WithdrawExpired]] message, emit withdrawExpire.success and send Processed * * @param state$ - Observable of current state * @param action - Withdraw request which caused this handling * @param signer - RaidenEpicDeps members * @param signer.signer - Signer instance * @param signer.log - Logger instance * @param cache - A Map to store and reuse previously Signed<WithdrawConfirmation> * @returns Observable of withdrawExpire.success|withdrawExpireProcessed|messageSend.request actions */ function receiveWithdrawExpired(state$, action, { signer, log }, cache) { const expired = action.payload.message; const tokenNetwork = expired.token_network_address; const partner = expired.participant; let expiration; try { expiration = expired.expiration.toNumber(); // expiration could be too big } catch (e) { return rxjs_1.EMPTY; } const withdrawMeta = { direction: state_2.Direction.RECEIVED, tokenNetwork, partner, totalWithdraw: expired.total_withdraw, expiration, }; return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)((state) => { (0, utils_4.assert)(partner === action.meta.address, 'participant mismatch'); const channel = getOpenChannel(state, { tokenNetwork, partner }, expired); let processed$; const cacheKey = `${(0, utils_1.channelUniqueKey)(channel)}+${expired.message_identifier.toString()}`; const cached = cache.get(cacheKey); if (cached) processed$ = (0, rxjs_1.of)(cached); else { (0, utils_4.assert)(expired.nonce.lte(channel.partner.nextNonce), [ 'nonce mismatch', { expected: channel.partner.nextNonce.toNumber(), received: expired.nonce.toNumber() }, ]); (0, utils_4.assert)(withdrawMeta.expiration <= Date.now() / 1e3, 'not expired yet'); const processed = { type: types_1.MessageType.PROCESSED, message_identifier: expired.message_identifier, }; processed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, processed, { log })).pipe((0, operators_1.tap)((signed) => cache.set(cacheKey, signed))); } return processed$.pipe((0, operators_1.mergeMap)(function* (processed) { // as we've received and validated this message, emit failure to increment nextNonce yield actions_4.withdrawExpire.success({ message: expired }, withdrawMeta); // emits withdrawCompleted to clear messages from partner's pendingWithdraws array yield actions_1.messageSend.request({ message: processed }, { address: partner, msgId: processed.message_identifier.toString() }); yield (0, actions_4.withdrawCompleted)(undefined, withdrawMeta); })); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawExpire.failure(err, withdrawMeta)))); } /** * Serialize creation and signing of BalanceProof-changing messages or actions * Actions which change any data in any channel balance proof must only ever be created reading * state inside the serialization flow provided by the concatMap, and also be composed and produced * inside it (inner, subscribed observable) * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps * @returns Observable of output actions for this epic */ function transferGenerateAndSignEnvelopeMessageEpic(action$, {}, deps) { const processedCache = new lru_1.LruCache(32); const state$ = deps.latest$.pipe((0, rx_1.pluckDistinct)('state'), (0, rx_1.completeWith)(action$)); // replayed(1)' state$ return (0, rxjs_1.merge)(action$.pipe((0, operators_1.filter)((0, actions_3.isActionOf)([ actions_4.transfer.request, actions_4.transferUnlock.request, actions_4.transferExpire.request, actions_4.withdraw.request, actions_4.withdrawExpire.request, ]))), // merge separatedly, to filter per message type before concat action$.pipe((0, operators_1.filter)((0, utils_2.isMessageReceivedOfType)([ (0, types_2.Signed)(types_1.LockedTransfer), (0, types_2.Signed)(types_1.Unlock), (0, types_2.Signed)(types_1.LockExpired), (0, types_2.Signed)(types_1.WithdrawRequest), (0, types_2.Signed)(types_1.WithdrawConfirmation), (0, types_2.Signed)(types_1.WithdrawExpired), ])))).pipe((0, operators_1.concatMap)((action) => { let output$; switch (action.type) { case actions_4.transfer.request.type: if (transferRequestIsResolved(action)) output$ = sendTransferSigned(state$, action, deps); else output$ = rxjs_1.EMPTY; break; case actions_4.transferUnlock.request.type: output$ = sendTransferUnlocked(state$, action, deps); break; case actions_4.transferExpire.request.type: output$ = sendTransferExpired(state$, action, deps); break; case actions_4.withdraw.request.type: output$ = sendWithdrawRequest(state$, action, deps); break; case actions_4.withdrawExpire.request.type: output$ = sendWithdrawExpired(state$, action, deps); break; case actions_1.messageReceived.type: switch (action.payload.message.type) { case types_1.MessageType.LOCKED_TRANSFER: output$ = receiveTransferSigned(state$, action, deps); break; case types_1.MessageType.UNLOCK: output$ = receiveTransferUnlocked(state$, action, deps); break; case types_1.MessageType.LOCK_EXPIRED: output$ = receiveTransferExpired(state$, action, deps); break; case types_1.MessageType.WITHDRAW_REQUEST: output$ = receiveWithdrawRequest(state$, action, deps); break; case types_1.MessageType.WITHDRAW_CONFIRMATION: output$ = receiveWithdrawConfirmation(state$, action, deps); break; case types_1.MessageType.WITHDRAW_EXPIRED: output$ = receiveWithdrawExpired(state$, action, deps, processedCache); break; } break; } return output$; })); } exports.transferGenerateAndSignEnvelopeMessageEpic = transferGenerateAndSignEnvelopeMessageEpic; //# sourceMappingURL=locked.js.map