UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

301 lines 18.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.transferSecretRegisterEpic = exports.transferAutoRegisterEpic = exports.transferSuccessOnSecretRegisteredEpic = exports.monitorSecretRegistryEpic = exports.transferRequestUnlockEpic = exports.transferSecretRevealedEpic = exports.transferSecretRevealEpic = exports.transferSecretRequestedEpic = void 0; const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const utils_1 = require("../../channels/utils"); const config_1 = require("../../config"); const constants_1 = require("../../constants"); const actions_1 = require("../../messages/actions"); const types_1 = require("../../messages/types"); const utils_2 = require("../../messages/utils"); const utils_3 = require("../../transport/utils"); const actions_2 = require("../../utils/actions"); const error_1 = require("../../utils/error"); const ethers_1 = require("../../utils/ethers"); const rx_1 = require("../../utils/rx"); const types_2 = require("../../utils/types"); const actions_3 = require("../actions"); const state_1 = require("../state"); const utils_4 = require("../utils"); const utils_5 = require("./utils"); /** * Handles receiving a signed SecretRequest from target for some sent LockedTransfer * Emits a transferSecretRequest action only if all conditions are met * * @param action$ - Observable of messageReceived actions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.address - Our address * @param deps.log - Logger instance * @returns Observable of transferSecretRequest actions */ function transferSecretRequestedEpic(action$, state$, { address, log }) { return action$.pipe((0, operators_1.filter)((0, utils_2.isMessageReceivedOfType)((0, types_2.Signed)(types_1.SecretRequest))), (0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(function* ([action, state]) { const message = action.payload.message; // proceed only if we know the secret and the SENT transfer const key = (0, utils_4.transferKey)({ secrethash: message.secrethash, direction: state_1.Direction.SENT }); const transferState = state.transfers[key]; if (!transferState) return; const locked = transferState.transfer; // we do only some basic verification here, as most of it is done upon SecretReveal, // to persist the request in most cases in TransferState.secretRequest if (locked.initiator !== address || // only the initiator may reply a SecretRequest locked.target !== action.meta.address || // reveal only to target !locked.payment_identifier.eq(message.payment_identifier)) { log.warn('Invalid SecretRequest for transfer', message, locked); return; } yield (0, actions_3.transferSecretRequest)({ message, userId: action.payload.userId }, { secrethash: message.secrethash, direction: state_1.Direction.SENT }); })); } exports.transferSecretRequestedEpic = transferSecretRequestedEpic; /** * Contains the core logic of {@link transferSecretRevealEpic}. * * @param action - The {@link transferSecretRequest} action that * @param deps - Epics dependencies * @param deps.signer - Signer instance * @param deps.log - Logger instance * @param deps.latest$ - Latest observable * @returns Observable of {@link transfer.failure}, {@link transferSecretReveal} or * {@link messageSend.request} actions */ const secretReveal$ = (action, { signer, log, latest$ }) => { const request = action.payload.message; return latest$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(({ state }) => { const transferState = state.transfers[(0, utils_4.transferKey)(action.meta)]; // shouldn't happen, as we're the initiator (for now), and always know the secret (0, error_1.assert)(transferState?.secret, ['SecretRequest for unknown secret', request]); const locked = transferState.transfer; const target = locked.target; const fee = transferState.fee; const value = locked.lock.amount.sub(fee); (0, error_1.assert)(request.expiration.lte(locked.lock.expiration) && request.expiration.gt(Math.floor(Date.now() / 1e3)), ['SecretRequest for expired transfer', { request, lock: locked.lock }]); (0, error_1.assert)(request.amount.gte(value), [ 'SecretRequest for amount too small!', { request, value }, ]); if (!request.amount.eq(value)) { // accept request log.info('Accepted SecretRequest for amount greater than sent', request, locked); } let reveal$; if (transferState.secretReveal) reveal$ = (0, rxjs_1.of)(transferState.secretReveal); else { const message = { type: types_1.MessageType.SECRET_REVEAL, message_identifier: (0, utils_4.makeMessageId)(), secret: transferState.secret, }; reveal$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log })); } return reveal$.pipe((0, operators_1.mergeMap)(function* (message) { yield (0, actions_3.transferSecretReveal)({ message }, action.meta); yield actions_1.messageSend.request({ message, userId: action.payload.userId }, { address: target, msgId: message.message_identifier.toString() }); })); }), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_3.transfer.failure(err, action.meta)))); }; /** * Handles a transferSecretRequest action to send the respective secret to target * It both emits transferSecretReveal (to persist sent SecretReveal in state and indicate that * the secret was revealed and thus the transfer should be assumed as succeeded) as well as * triggers sending the message once. New SecretRequests will cause a new transferSecretRequest, * which will re-send the cached SecretReveal. * transfer.failure is emitted in case invalid secretRequest comes, as no valid one will come as * per current implementation, so we fail early to notify users about it. * * @param action$ - Observable of transferSecretRequest actions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps * @param deps.signer - Signer instance * @param deps.latest$ - Latest observable * @param deps.log - Logger instance * @returns Observable of transfer.failure|transferSecretReveal|messageSend.request actions */ function transferSecretRevealEpic(action$, {}, deps) { return action$.pipe((0, operators_1.filter)((0, actions_2.isActionOf)(actions_3.transferSecretRequest)), (0, operators_1.filter)((action) => action.meta.direction === state_1.Direction.SENT), (0, operators_1.concatMap)((action) => secretReveal$(action, deps))); } exports.transferSecretRevealEpic = transferSecretRevealEpic; /** * Handles receiving a valid SecretReveal from recipient (neighbor/partner) * This indicates that the partner knowws the secret, and we should Unlock to avoid going on-chain. * The transferUnlock.request action is a request for the unlocking to be generated and sent. * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.config$ - Config observable * @returns Observable of output actions for this epic */ function transferSecretRevealedEpic(action$, state$, { config$ }) { return action$.pipe( // we don't require Signed SecretReveal, nor even check sender for persisting the secret (0, operators_1.filter)((0, utils_2.isMessageReceivedOfType)(types_1.SecretReveal)), (0, operators_1.withLatestFrom)(state$, config$), (0, operators_1.mergeMap)(function* ([action, state, { caps }]) { const secrethash = (0, utils_4.getSecrethash)(action.payload.message.secret); const results = Object.values(state_1.Direction) .map((direction) => state.transfers[(0, utils_4.transferKey)({ secrethash, direction })]) .filter(types_2.isntNil); const message = action.payload.message; for (const sent of results.filter((doc) => doc.direction === state_1.Direction.SENT)) { const meta = { secrethash, direction: state_1.Direction.SENT }; // if secrethash matches, we're good for persisting, don't care for sender/signature yield (0, actions_3.transferSecret)({ secret: message.secret }, meta); // but are stricter for unlocking to next hop only if (action.meta.address === sent.partner && // don't unlock if channel closed: balanceProofs already registered on-chain !sent.channelClosed // accepts secretReveal/unlock request even if registered on-chain ) { let viaPayload; // unlock _through_ sender iff message is signed if ('signature' in message) viaPayload = { userId: action.payload.userId }; // request unlock to be composed, signed & sent to partner yield actions_3.transferUnlock.request(viaPayload, meta); } } // avoid unlocking received transfers if receiving is disabled if (!(0, utils_3.getCap)(caps, constants_1.Capabilities.RECEIVE)) return; // we're mediator or target, and received reveal from next hop or initiator, respectively for (const _received of results.filter((doc) => doc.direction === state_1.Direction.RECEIVED)) { // if secrethash matches, we're good for persisting, which also triggers Reveal back yield (0, actions_3.transferSecret)({ secret: message.secret }, { secrethash, direction: state_1.Direction.RECEIVED }); } })); } exports.transferSecretRevealedEpic = transferSecretRevealedEpic; /** * For a received transfer, when we know the secret, sign & send a SecretReveal to previous hop * * @param action$ - Observable of transferSecret|transferSecretReveal actions * @param state$ - Observable of RaidenStates * @param deps - RaidenEpicDeps * @param deps.log - Logger instance * @param deps.signer - Signer instance * @param deps.latest$ - Latest observable * @returns Observable of transferSecretReveal actions */ function transferRequestUnlockEpic(action$, {}, { log, signer, latest$ }) { return action$.pipe((0, operators_1.filter)((0, actions_2.isActionOf)([actions_3.transferSecret, actions_3.transferSecretRegister.success])), (0, operators_1.filter)((action) => action.meta.direction === state_1.Direction.RECEIVED), (0, operators_1.filter)((action) => actions_3.transferSecret.is(action) || !!action.payload.confirmed), (0, operators_1.concatMap)((action) => latest$.pipe((0, rx_1.pluckDistinct)('state'), (0, operators_1.first)(), (0, operators_1.filter)(({ transfers }) => (0, utils_4.transferKey)(action.meta) in transfers), (0, operators_1.mergeMap)(({ transfers }) => { const transferState = transfers[(0, utils_4.transferKey)(action.meta)]; const cached = transferState.secretReveal; let signed$; if (cached) { signed$ = (0, rxjs_1.of)((0, types_2.untime)(cached)); } else { const message = { type: types_1.MessageType.SECRET_REVEAL, message_identifier: (0, utils_4.makeMessageId)(), secret: action.payload.secret, }; signed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log })); } const via = (0, utils_4.searchValidViaAddress)(transferState.transfer.metadata, transferState.partner); return signed$.pipe((0, operators_1.map)((message) => (0, actions_3.transferSecretReveal)({ message, ...via }, action.meta))); }), (0, operators_1.catchError)((err) => { log.warn('Error trying to sign SecretReveal - ignoring', err, action.meta); return rxjs_1.EMPTY; })))); } exports.transferRequestUnlockEpic = transferRequestUnlockEpic; /** * Monitors SecretRegistry and emits when a relevant secret gets registered * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.provider - Provider instance * @param deps.secretRegistryContract - SecretRegistry contract instance * @param deps.config$ - Config observable * @param deps.init$ - Init$ tasks subject * @param deps.getBlockTimestamp - block timestamp getter function * @returns Observable of transferSecretRegister.success actions */ function monitorSecretRegistryEpic({}, state$, { provider, secretRegistryContract, config$, init$, getBlockTimestamp }) { const initSub = new rxjs_1.AsyncSubject(); init$.next(initSub); return (0, ethers_1.fromEthersEvent)(provider, secretRegistryContract.filters.SecretRevealed(null, null), { confirmations: config$.pipe((0, operators_1.pluck)('confirmationBlocks')), blockNumber$: state$.pipe((0, rx_1.pluckDistinct)('blockNumber')), onPastCompleted: () => { initSub.next(null); initSub.complete(); }, }).pipe((0, rx_1.completeWith)(state$), (0, operators_1.map)((0, ethers_1.logToContractEvent)(secretRegistryContract)), (0, rx_1.withMergeFrom)(([, , event]) => getBlockTimestamp(event.blockNumber)), (0, operators_1.withLatestFrom)(state$, config$), (0, operators_1.mergeMap)(function* ([[[secrethash, secret, event], txTimestamp], { transfers, blockNumber }, { confirmationBlocks },]) { // find sent|received transfers matching secrethash and secret registered before expiration for (const direction of Object.values(state_1.Direction)) { const key = (0, utils_4.transferKey)({ secrethash: secrethash, direction }); const transferState = transfers[key]; if (!transferState || transferState.expiration <= txTimestamp) continue; yield actions_3.transferSecretRegister.success({ secret: secret, txTimestamp, txHash: event.transactionHash, txBlock: event.blockNumber, confirmed: event.blockNumber + confirmationBlocks <= blockNumber ? !event.removed : undefined, }, { secrethash: secrethash, direction }); } })); } exports.monitorSecretRegistryEpic = monitorSecretRegistryEpic; /** * A simple epic to emit transfer.success when secret register is confirmed * * @param action$ - Observable of transferSecretRegister.success actions * @returns Observable of transfer.success actions */ function transferSuccessOnSecretRegisteredEpic(action$) { return action$.pipe((0, operators_1.filter)(actions_3.transferSecretRegister.success.is), (0, operators_1.filter)((action) => !!action.payload.confirmed), (0, operators_1.map)((action) => actions_3.transfer.success({}, action.meta))); } exports.transferSuccessOnSecretRegisteredEpic = transferSuccessOnSecretRegisteredEpic; /** * Process newBlocks and pending received transfers. If we know the secret, and transfer doesn't * get unlocked before revealTimeout seconds are left to lock expiration, request to register secret * TODO: check economic viability (and define what that means) of registering lock on-chain * * @param action$ - Observable of newBlock actions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.config$ - Config observable * @returns Observable of transferSecretRegister.request actions */ function transferAutoRegisterEpic(action$, state$, { config$ }) { const transferIsGone$ = ({ key }) => state$.pipe((0, operators_1.filter)(({ transfers }) => !(key in transfers))); return state$.pipe((0, operators_1.pluck)('transfers'), (0, operators_1.mergeMap)((transfers) => Object.values(transfers)), (0, operators_1.filter)(({ direction }) => direction === state_1.Direction.RECEIVED), (0, operators_1.groupBy)(({ _id }) => _id, { duration: transferIsGone$ }), (0, operators_1.mergeMap)((grouped$) => grouped$.pipe((0, operators_1.filter)(({ secret }) => !!secret), (0, operators_1.withLatestFrom)(config$), (0, operators_1.delayWhen)(([{ expiration }, { revealTimeout }]) => // danger zone! (0, rxjs_1.timer)(new Date((expiration - revealTimeout) * 1e3))), (0, operators_1.exhaustMap)(([{ secret, transfer: locked }]) => { const meta = { secrethash: locked.lock.secrethash, direction: state_1.Direction.RECEIVED }; return (0, utils_5.dispatchAndWait$)(action$, actions_3.transferSecretRegister.request({ secret: secret }, meta), (0, actions_2.isConfirmationResponseOf)(actions_3.transferSecretRegister, meta)); }), (0, operators_1.takeUntil)(grouped$.pipe((0, operators_1.filter)((t) => !!(t.unlock || t.expired || t.secretRegistered || t.channelClosed || // gives up immediatelly if already over expiration t.expiration <= Date.now() / 1e3)), (0, operators_1.mergeWith)(transferIsGone$(grouped$)))), (0, rx_1.completeWith)(state$)))); } exports.transferAutoRegisterEpic = transferAutoRegisterEpic; /** * Registers secret on-chain. Success is detected by monitorSecretRegistryEpic * * @param action$ - Observable of transferSecretRegister.request actions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @returns Observable of transferSecretRegister.failure actions */ function transferSecretRegisterEpic(action$, {}, deps) { const { log, secretRegistryContract, config$ } = deps; return action$.pipe((0, operators_1.filter)(actions_3.transferSecretRegister.request.is), (0, operators_1.mergeMap)((action) => (0, utils_1.transact)(secretRegistryContract, 'registerSecret', [action.payload.secret], deps, { error: error_1.ErrorCodes.XFER_REGISTERSECRET_TX_FAILED, }).pipe((0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { onErrors: error_1.commonTxErrors, log: log.debug }), // transferSecretRegister.success handled by monitorSecretRegistryEpic (0, operators_1.ignoreElements)(), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_3.transferSecretRegister.failure(err, action.meta)))))); } exports.transferSecretRegisterEpic = transferSecretRegisterEpic; //# sourceMappingURL=secret.js.map