raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
171 lines • 9.83 kB
JavaScript
import { BigNumber } from '@ethersproject/bignumber';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import pickBy from 'lodash/pickBy';
import { AsyncSubject, combineLatest, defer, EMPTY, from, of, timer } from 'rxjs';
import { catchError, concatMap, debounce, distinctUntilChanged, filter, first, map, mergeMap, pairwise, pluck, startWith, switchMap, takeUntil, withLatestFrom, } from 'rxjs/operators';
import { newBlock } from '../../channels/actions';
import { ChannelState } from '../../channels/state';
import { channelAmounts, groupChannel } from '../../channels/utils';
import { Capabilities } from '../../constants';
import { messageServiceSend } from '../../messages/actions';
import { MessageType } from '../../messages/types';
import { signMessage } from '../../messages/utils';
import { makeMessageId } from '../../transfers/utils';
import { matrixPresence } from '../../transport/actions';
import { getCap } from '../../transport/utils';
import { isActionOf } from '../../utils/actions';
import { fromEthersEvent, logToContractEvent } from '../../utils/ethers';
import { completeWith, dispatchRequestAndGetResponse, pluckDistinct } from '../../utils/rx';
import { pathFind, servicesValid } from '../actions';
import { PfsMode, Service } from '../types';
import { getRoute$, makeTimestamp, validateRoute$ } from './helpers';
/**
* Check if a transfer can be made and return a set of paths for it.
*
* @param action$ - Observable of pathFind.request actions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps object
* @returns Observable of pathFind.{success|failure} actions
*/
export function pfsRequestEpic(action$, {}, deps) {
return action$.pipe(dispatchRequestAndGetResponse(matrixPresence, (dispatch) => action$.pipe(filter(isActionOf(pathFind.request)), concatMap((action) => dispatch(matrixPresence.request(undefined, { address: action.meta.target })).pipe(withLatestFrom(deps.latest$), mergeMap(([targetPresence, latest]) => getRoute$(action, deps, latest, targetPresence)), withLatestFrom(deps.latest$), mergeMap(([route, { state }]) => validateRoute$([action, route], state, deps)), catchError((err) => of(pathFind.failure(err, action.meta))))))));
}
/**
* Sends a [[PFSCapacityUpdate]] to PFSs on new deposit on our side of channels
*
* @param action$ - Observable of channelDeposit.success actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.log - Logger instance
* @param deps.address - Our address
* @param deps.network - Current Network
* @param deps.signer - Signer instance
* @param deps.config$ - Config observable
* @returns Observable of messageServiceSend.request actions
*/
export function pfsCapacityUpdateEpic({}, state$, { log, address, network, signer, config$ }) {
return state$.pipe(groupChannel(), mergeMap((grouped$) => grouped$.pipe(pairwise(), // skips first emission on startup
withLatestFrom(config$),
// ignore actions if channel not open or while/if pfs is disabled
filter(([[, channel], { pfsMode }]) => channel.state === ChannelState.open && pfsMode !== PfsMode.disabled), debounce(([[prev, cur], { httpTimeout }]) => cur.own.locks.length > prev.own.locks.length ||
cur.partner.locks.length > prev.partner.locks.length
? // if either lock increases, a transfer is pending, debounce by httpTimeout=30s
timer(httpTimeout)
: of(1)), switchMap(([[, channel], { revealTimeout }]) => {
const tokenNetwork = channel.tokenNetwork;
const partner = channel.partner.address;
const { ownCapacity, partnerCapacity } = channelAmounts(channel);
const message = {
type: MessageType.PFS_CAPACITY_UPDATE,
canonical_identifier: {
chain_identifier: BigNumber.from(network.chainId),
token_network_address: tokenNetwork,
channel_identifier: BigNumber.from(channel.id),
},
updating_participant: address,
other_participant: partner,
updating_nonce: channel.own.balanceProof.nonce,
other_nonce: channel.partner.balanceProof.nonce,
updating_capacity: ownCapacity,
other_capacity: partnerCapacity,
reveal_timeout: BigNumber.from(revealTimeout),
};
const msgId = makeMessageId().toString();
return defer(() => signMessage(signer, message, { log })).pipe(map((signed) => messageServiceSend.request({ message: signed }, { service: Service.PFS, msgId })), catchError((err) => {
log.error('Error trying to generate & sign PFSCapacityUpdate', err);
return EMPTY;
}));
}))));
}
/**
* When monitoring a channel (either a new channel or a previously monitored one), send a matching
* PFSFeeUpdate to PFSs, so they can pick us for mediation
*
* @param action$ - Observable of channelMonitored actions
* @param state$ - Observable of RaidenStates
* @param deps - Raiden epic dependencies
* @param deps.log - Logger instance
* @param deps.address - Our address
* @param deps.network - Current network
* @param deps.signer - Signer instance
* @param deps.config$ - Config observable
* @param deps.mediationFeeCalculator - Calculator for mediation fees schedule
* @returns Observable of messageServiceSend.request actions
*/
export function pfsFeeUpdateEpic({}, state$, { log, address, network, signer, config$, mediationFeeCalculator }) {
return state$.pipe(groupChannel(), mergeMap((grouped$) => combineLatest([
grouped$,
config$.pipe(pluckDistinct('caps')),
config$.pipe(pluckDistinct('mediationFees')),
]).pipe(filter(([, caps]) => !!getCap(caps, Capabilities.MEDIATE)), map(([channel, , mediationFees]) => {
const schedule = mediationFeeCalculator.schedule(mediationFees, channel);
// using channel feeSchedule above, build a PFSFeeUpdate's schedule payload
return [channel, schedule];
}),
// reactive on channel state and config changes, distinct on schedule's payload
distinctUntilChanged(([, sched1], [, sched2]) => isEqual(sched1, sched2)), switchMap(([channel, schedule]) => {
const message = {
type: MessageType.PFS_FEE_UPDATE,
canonical_identifier: {
chain_identifier: BigNumber.from(network.chainId),
token_network_address: channel.tokenNetwork,
channel_identifier: BigNumber.from(channel.id),
},
updating_participant: address,
timestamp: makeTimestamp(),
fee_schedule: schedule,
};
const msgId = makeMessageId().toString();
const meta = { service: Service.PFS, msgId };
return from(signMessage(signer, message, { log })).pipe(map((signed) => messageServiceSend.request({ message: signed }, meta)), catchError((err) => {
log.error('Error trying to generate & sign PFSFeeUpdate', err);
return EMPTY;
}));
}), takeUntil(grouped$.pipe(filter((channel) => channel.state !== ChannelState.open))))), completeWith(state$));
}
/**
* Fetch & monitors ServiceRegistry's RegisteredService events, keep track of valid_till expiration
* and aggregate list of valid service addresses
*
* Notice this epic only deals with the events & addresses, and don't fetch URLs, which need to be
* fetched on-demand through [[pfsInfo]] & [[pfsListInfo]].
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps object
* @param deps.provider - Provider instance
* @param deps.serviceRegistryContract - ServiceRegistry contract instance
* @param deps.contractsInfo - Contracts info mapping
* @param deps.config$ - Config observable
* @param deps.init$ - Init$ tasks subject
* @returns Observable of servicesValid actions
*/
export function pfsServiceRegistryMonitorEpic(action$, state$, { provider, serviceRegistryContract, contractsInfo, config$, init$ }) {
const blockNumber$ = action$.pipe(filter(newBlock.is), pluck('payload', 'blockNumber'));
return state$.pipe(first(), switchMap(({ services: initialServices }) => {
const initSub = new AsyncSubject();
init$.next(initSub);
return fromEthersEvent(provider, serviceRegistryContract.filters.RegisteredService(null, null, null, null), {
// if initialServices is empty, fetch since registry deploy block, else, resetEventsBlock
fromBlock: isEmpty(initialServices)
? contractsInfo.TokenNetworkRegistry.block_number
: undefined,
confirmations: config$.pipe(pluck('confirmationBlocks')),
blockNumber$,
onPastCompleted: () => {
initSub.next(null);
initSub.complete();
},
}).pipe(withLatestFrom(state$, config$), filter(([{ blockNumber: eventBlock }, { blockNumber }, { confirmationBlocks }]) => !!eventBlock && eventBlock + confirmationBlocks <= blockNumber), pluck(0), map(logToContractEvent(serviceRegistryContract)), withLatestFrom(state$),
// merge new entry with stored state
map(([[service, valid_till], { services }]) => ({
...services,
[service]: valid_till.toNumber() * 1000,
})), startWith(initialServices),
// switchMap with newBlock events ensure this filter gets re-evaluated every block
// and filters out entries which aren't valid anymore
switchMap((services) => action$.pipe(filter(newBlock.is), startWith(true), map(() => pickBy(services, (till) => Date.now() < till)))));
}), distinctUntilChanged(isEqual), map((valid) => servicesValid(valid)), completeWith(action$));
}
//# sourceMappingURL=pathfinding.js.map