raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
139 lines • 8.15 kB
JavaScript
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { EMPTY, from, merge, of } from 'rxjs';
import { catchError, debounceTime, filter, first, groupBy, map, mergeMap, mergeMapTo, pluck, startWith, take, withLatestFrom, } from 'rxjs/operators';
import { Capabilities } from '../../constants';
import { pathFind } from '../../services/actions';
import { matrixPresence } from '../../transport/actions';
import { getCap } from '../../transport/utils';
import { completeWith, dispatchRequestAndGetResponse, distinctRecordValues, pluckDistinct, } from '../../utils/rx';
import { untime } from '../../utils/types';
import { transfer, transferClear, transferExpire, transferSecret, transferSecretRequest, transferSecretReveal, transferSigned, transferUnlock, } from '../actions';
import { Direction } from '../state';
import { metadataFromPaths, transferKey } from '../utils';
/**
* Re-queue pending transfer's BalanceProof/Envelope messages for retry on init
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @returns Observable of transferSigned|transferUnlock.success actions
*/
export function initQueuePendingEnvelopeMessagesEpic({}, state$) {
return state$.pipe(first(), mergeMap(({ transfers }) => from(Object.values(transfers).filter((r) => r.direction === Direction.SENT &&
!r.unlockProcessed &&
!r.expiredProcessed &&
!r.secretRegistered &&
!r.channelClosed))), mergeMap(function* (transferState) {
// loop over all pending transfers
const meta = {
secrethash: transferState.transfer.lock.secrethash,
direction: Direction.SENT,
};
// on init, request monitor presence of any pending transfer target
yield matrixPresence.request(undefined, { address: transferState.transfer.target });
// Processed not received yet for LockedTransfer
if (!transferState.transferProcessed)
yield transferSigned({
message: untime(transferState.transfer),
fee: transferState.fee,
partner: transferState.partner,
}, meta);
// already unlocked, but Processed not received yet for Unlock
if (transferState.unlock)
yield transferUnlock.success({ message: untime(transferState.unlock), partner: transferState.partner }, meta);
// lock expired, but Processed not received yet for LockExpired
if (transferState.expired)
yield transferExpire.success({ message: untime(transferState.expired), partner: transferState.partner }, meta);
}));
}
/**
* Re-queue pending Received transfer's
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.config$ - Config observable
* @returns Observable of transferSigned|transferUnlock.success actions
*/
export function initQueuePendingReceivedEpic({}, state$, { config$ }) {
return state$.pipe(first(), mergeMap(({ transfers }) => from(Object.values(transfers).filter((r) => r.direction === Direction.RECEIVED &&
!r.unlock &&
!r.expired &&
!r.secretRegistered &&
!r.channelClosed))), mergeMap((transferState) => {
// loop over all pending transfers
const secrethash = transferState.transfer.lock.secrethash;
const meta = { secrethash, direction: Direction.RECEIVED };
return merge(
// on init, request monitor presence of any pending transfer initiator
of(transferSigned({
message: untime(transferState.transfer),
fee: transferState.fee,
partner: transferState.partner,
}, meta)),
// already revealed to us, but user didn't sign SecretReveal yet
transferState.secret && !transferState.secretReveal
? of(transferSecret({ secret: transferState.secret }, meta))
: EMPTY,
// already revealed to sender, but they didn't Unlock yet
transferState.secretReveal
? of(transferSecretReveal({ message: untime(transferState.secretReveal) }, meta))
: EMPTY,
// secret not yet known; request *when* receiving is enabled (may be later)
// secretRequest should always be defined as we sign it when receiving transfer
!transferState.secret && transferState.secretRequest
? config$.pipe(pluck('caps'), filter((caps) => !!getCap(caps, Capabilities.RECEIVE)), take(1), mergeMapTo(of(matrixPresence.request(undefined, { address: transferState.transfer.initiator }), transferSecretRequest({ message: untime(transferState.secretRequest) }, meta))))
: EMPTY);
}));
}
/**
* @param action$ - Observable of unresolved transfer.request actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependenceis
* @param deps.config$ - Config observable
* @returns Observable of pathFind.request and resolved transfer.request actions
*/
export function transferRequestResolveEpic(action$, {}, { config$ }) {
return action$.pipe(dispatchRequestAndGetResponse(pathFind, (dispatch) => action$.pipe(filter(transfer.request.is), filter((action) => !action.payload.resolved), mergeMap((action) => dispatch(pathFind.request(pick(action.payload, ['paths', 'pfs']), pick(action.payload, ['tokenNetwork', 'target', 'value']))).pipe(withLatestFrom(action$.pipe(filter(matrixPresence.success.is), filter((a) => a.meta.address === action.payload.target)), config$), map(([route, targetPresence, { encryptSecret }]) => {
let encryptSecretOptions;
if ((action.payload.encryptSecret ?? encryptSecret) && action.payload.secret)
encryptSecretOptions = {
secret: action.payload.secret,
amount: action.payload.value,
payment_identifier: action.payload.paymentId,
};
const resolvedPayload = metadataFromPaths(route.payload.paths, targetPresence, encryptSecretOptions);
const restPayload = omit(action.payload, ['paths', 'pfs', 'encryptSecret']);
const requestOptions = { ...restPayload, ...resolvedPayload };
return transfer.request(requestOptions, action.meta);
}), catchError((err) => of(transfer.failure(err, action.meta))))))));
}
function hasTransferMeta(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
action) {
return 'meta' in action && action.meta?.secrethash && action.meta?.direction;
}
/**
* Clear transfer from state after it's completed and some timeout of inactivity
* It should still get saved (by persister) on database, but is freed from memory.
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.config$ - Config observable
* @returns Observable of transferClear actions
*/
export function transferClearCompletedEpic(action$, state$, { config$ }) {
return state$.pipe(pluckDistinct('transfers'), distinctRecordValues(), groupBy(([key]) => key, // group per transfer
// cleanup when transfer is cleared
{ duration: ({ key }) => state$.pipe(filter(({ transfers }) => !(key in transfers))) }), withLatestFrom(config$), mergeMap(([grouped$, { httpTimeout }]) => grouped$.pipe(
// when transfer completes or there's nothing else to do with it
filter(([, transfer]) => !!(transfer.unlockProcessed || transfer.expiredProcessed || transfer.channelSettled)), take(1), mergeMap(([, transfer]) =>
// in case any new "activity" happens on this transfer, bump debounceTimer'r
action$.pipe(filter(hasTransferMeta), filter((action) => transferKey(action.meta) === grouped$.key), startWith({ meta: pick(transfer, ['secrethash', 'direction']) }), completeWith(grouped$))),
// after some time with no action for this transfer going through (e.g. Processed retries)
debounceTime(3 * httpTimeout), map(({ meta }) => transferClear(undefined, meta)))),
// no need to transferClear on shutdown; let any pending transfer be cleared on next run
completeWith(action$));
}
//# sourceMappingURL=init.js.map