UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

159 lines 8.92 kB
import { BigNumber } from '@ethersproject/bignumber'; import { concat as concatBytes } from '@ethersproject/bytes'; import { MaxUint256, WeiPerEther, Zero } from '@ethersproject/constants'; import { AsyncSubject, combineLatest, EMPTY, from, of, timer } from 'rxjs'; import { debounce, distinctUntilChanged, filter, map, mergeMap, pairwise, pluck, switchMap, take, withLatestFrom, } from 'rxjs/operators'; import { newBlock } from '../../channels/actions'; import { channelAmounts, groupChannel } from '../../channels/utils'; import { messageServiceSend } from '../../messages/actions'; import { MessageType } from '../../messages/types'; import { createBalanceHash, MessageTypeId, signMessage } from '../../messages/utils'; import { makeMessageId } from '../../transfers/utils'; import { encode } from '../../utils/data'; import { fromEthersEvent, logToContractEvent } from '../../utils/ethers'; import { catchAndLog, completeWith } from '../../utils/rx'; import { isntNil } from '../../utils/types'; import { msBalanceProofSent } from '../actions'; import { Service } from '../types'; /** * Makes a *Map callback which returns an observable of actions to send RequestMonitoring messages * * @param state$ - Observable of RaidenStates * @param channel - Channel state to generate a a monitoring request for * @param deps - Epics dependencies * @param deps.address - Our Address * @param deps.log - Logger instance * @param deps.network - Current network * @param deps.signer - Signer instance * @param deps.contractsInfo - Contracts info mapping * @param deps.latest$ - Latest observable * @param deps.config$ - Config observable * @returns An operator which receives prev and current Channel states and returns a cold * Observable of messageServiceSend.request actions to monitoring services */ function makeMonitoringRequest$(state$, channel, { address, log, network, signer, contractsInfo, latest$, config$ }) { const { partnerUnlocked } = channelAmounts(channel); // give up early if nothing to lose if (partnerUnlocked.isZero()) return EMPTY; return combineLatest([latest$, config$]).pipe( // combineLatest + filter ensures it'll pass if anything here changes filter(([{ udcDeposit }, { monitoringReward, rateToSvt }]) => // ignore actions while/if config.monitoringReward isn't enabled !!monitoringReward?.gt(Zero) && // wait for udcDepost.balance >= monitoringReward, fires immediately if already udcDeposit.balance.gte(monitoringReward) && // use partner's total off & on-chain unlocked, total we'd lose if don't update BP partnerUnlocked // use rateToSvt to convert to equivalent SVT, and pass only if > monitoringReward; // default rate=MaxUint256 means it'll ALWAYS monitor if no rate is set for token .mul(rateToSvt[channel.token] ?? MaxUint256) .div(WeiPerEther) .gt(monitoringReward)), take(1), // take/act on first time all conditions above pass completeWith(state$, 10), // if conditions weren't met on shutdown, give up mergeMap(([, { monitoringReward }]) => { const balanceProof = channel.partner.balanceProof; const balanceHash = createBalanceHash(balanceProof); const nonClosingMessage = concatBytes([ encode(channel.tokenNetwork, 20), encode(network.chainId, 32), encode(MessageTypeId.BALANCE_PROOF_UPDATE, 32), encode(channel.id, 32), encode(balanceHash, 32), encode(balanceProof.nonce, 32), encode(balanceProof.additionalHash, 32), encode(balanceProof.signature, 65), // partner's signature for this balance proof ]); // UInt8Array of 277 bytes const msgId = makeMessageId().toString(); // first sign the nonClosing signature, then the actual message return from(signer.signMessage(nonClosingMessage)).pipe(mergeMap((nonClosingSignature) => signMessage(signer, { type: MessageType.MONITOR_REQUEST, balance_proof: { chain_id: balanceProof.chainId, token_network_address: balanceProof.tokenNetworkAddress, channel_identifier: BigNumber.from(channel.id), nonce: balanceProof.nonce, balance_hash: balanceHash, additional_hash: balanceProof.additionalHash, signature: balanceProof.signature, }, non_closing_participant: address, non_closing_signature: nonClosingSignature, monitoring_service_contract_address: contractsInfo.MonitoringService.address, reward_amount: monitoringReward, }, { log })), map((message) => messageServiceSend.request({ message }, { service: Service.MS, msgId }))); }), catchAndLog({ log: log.warn }, 'Error trying to generate & sign MonitorRequest')); } /** * Handle balanceProof change from partner (received transfers) and request monitoring from MS * * @param action$ - Observable of channelDeposit.success actions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @returns Observable of messageServiceSend.request actions */ export function msMonitorRequestEpic({}, state$, deps) { return state$.pipe(groupChannel(), withLatestFrom(deps.config$), mergeMap(([grouped$, { httpTimeout }]) => grouped$.pipe( // act only if partner's transferredAmount or lockedAmount changes distinctUntilChanged((a, b) => b.partner.balanceProof.transferredAmount.eq(a.partner.balanceProof.transferredAmount) && b.partner.balanceProof.lockedAmount.eq(a.partner.balanceProof.lockedAmount) && b.partner.locks === a.partner.locks), pairwise(), // distinctUntilChanged allows first, so pair and skips it debounce(([prev, cur]) => // if partner lock increases, a transfer is pending, debounce by httpTimeout=30s // otherwise transfer completed, emits immediately cur.partner.locks.length > prev.partner.locks.length ? timer(httpTimeout) : of(1)), // switchMap may unsubscribe from previous udcDeposit wait/signature prompts if partner's // balanceProof balance changes in the meantime switchMap(([, channel]) => makeMonitoringRequest$(state$, channel, deps))))); } /** * Monitors MonitoringService contract and fires events when an MS sent a BP in our behalf. * * When this epic is subscribed (startup), it fetches events since 'provider.resetEventsBlock', * which is set to latest monitored block, so on startup we always pick up events that were fired * while offline, and keep monitoring while online, although it isn't probable that MS would quick * in while we're online, since [[channelUpdateEpic]] would update the channel ourselves. * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates * @param deps - Epics dependencies * @param deps.provider - Provider instance * @param deps.monitoringServiceContract - MonitoringService contract instance * @param deps.address - Our address * @param deps.config$ - Config observable * @param deps.init$ - Subject of initial sync tasks * @returns Observable of msBalanceProofSent actions */ export function msMonitorNewBPEpic(action$, state$, { provider, monitoringServiceContract, address, config$, init$ }) { const initSub = new AsyncSubject(); init$.next(initSub); return fromEthersEvent(provider, monitoringServiceContract.filters.NewBalanceProofReceived(null, null, null, null, null, address), { confirmations: config$.pipe(pluck('confirmationBlocks')), blockNumber$: action$.pipe(filter(newBlock.is), pluck('payload', 'blockNumber')), onPastCompleted: () => { initSub.next(null); initSub.complete(); }, }).pipe(completeWith(state$), map(logToContractEvent(monitoringServiceContract)), // should never fail, as per filter filter(([, , , , , raidenAddress]) => raidenAddress === address), withLatestFrom(state$, config$), map(([[tokenNetwork, id, reward, nonce, monitoringService, , event], state, { confirmationBlocks },]) => { const channel = Object.values(state.channels) .concat(Object.values(state.oldChannels)) .find((c) => c.tokenNetwork === tokenNetwork && id.eq(c.id)); const txBlock = event.blockNumber; if (!channel || !txBlock) return; return msBalanceProofSent({ tokenNetwork: tokenNetwork, partner: channel.partner.address, id: channel.id, reward: reward, nonce: nonce, monitoringService: monitoringService, txHash: event.transactionHash, txBlock, confirmed: txBlock + confirmationBlocks <= state.blockNumber ? true : undefined, }); }), filter(isntNil)); } //# sourceMappingURL=monitor.js.map