raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
138 lines • 7.37 kB
JavaScript
import { concat as concatBytes } from '@ethersproject/bytes';
import { Zero } from '@ethersproject/constants';
import { defer, EMPTY, from, of } from 'rxjs';
import { catchError, delayWhen, filter, ignoreElements, mergeMap, takeUntil, tap, withLatestFrom, } from 'rxjs/operators';
import { intervalFromConfig } from '../../config';
import { createBalanceHash, MessageTypeId } from '../../messages/utils';
import { isActionOf } from '../../utils/actions';
import { encode } from '../../utils/data';
import { commonAndFailTxErrors, ErrorCodes, RaidenError } from '../../utils/error';
import { retryWhile } from '../../utils/rx';
import { channelClose, newBlock } from '../actions';
import { ChannelState } from '../state';
import { channelKey, transact } from '../utils';
/**
* A ChannelClose action requested by user
* Needs to be called on an opened or closing (for retries) channel.
* If tx goes through successfuly, stop as ChannelClosed success action will instead be detected
* and reacted by channelEventsEpic.
* If anything detectable goes wrong, fires a ChannelCloseActionFailed instead
*
* @param action$ - Observable of channelClose actions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps members
* @returns Observable of channelClose.failure actions
*/
export function channelCloseEpic(action$, state$, deps) {
const { log, signer, address, network, getTokenNetworkContract, config$ } = deps;
return action$.pipe(filter(isActionOf(channelClose.request)), withLatestFrom(state$), mergeMap(([action, state]) => {
const { tokenNetwork, partner } = action.meta;
const channel = state.channels[channelKey(action.meta)];
if (channel?.state !== ChannelState.open && channel?.state !== ChannelState.closing) {
const error = new RaidenError(ErrorCodes.CNL_NO_OPEN_OR_CLOSING_CHANNEL_FOUND, action.meta);
return of(channelClose.failure(error, action.meta));
}
const balanceProof = channel.partner.balanceProof;
const balanceHash = createBalanceHash(balanceProof);
const nonce = balanceProof.nonce;
const additionalHash = balanceProof.additionalHash;
const nonClosingSignature = balanceProof.signature;
const closingMessage = concatBytes([
encode(tokenNetwork, 20),
encode(network.chainId, 32),
encode(MessageTypeId.BALANCE_PROOF, 32),
encode(channel.id, 32),
encode(balanceHash, 32),
encode(nonce, 32),
encode(additionalHash, 32),
encode(nonClosingSignature, 65), // partner's signature for this balance proof
]); // UInt8Array of 277 bytes
// sign counter balance proof, then send closeChannel transaction with our signature
return from(signer.signMessage(closingMessage)).pipe(mergeMap((closingSignature) => transact(getTokenNetworkContract(tokenNetwork), 'closeChannel', [
channel.id,
partner,
address,
balanceHash,
nonce,
additionalHash,
nonClosingSignature,
closingSignature,
], deps, { error: ErrorCodes.CNL_CLOSECHANNEL_FAILED }).pipe(retryWhile(intervalFromConfig(config$), {
onErrors: commonAndFailTxErrors,
log: log.info,
}),
// if channel gets closed while retrying (e.g. by partner), give up
takeUntil(action$.pipe(filter(channelClose.success.is), filter((action) => action.meta.tokenNetwork === tokenNetwork && action.meta.partner === partner))))),
// if succeeded, return a empty/completed observable
// actual ChannelClosedAction will be detected and handled by channelEventsEpic
// if any error happened on tx call/pipeline, catchError will then emit the
// channelClose.failure action instead
ignoreElements(), catchError((error) => of(channelClose.failure(error, action.meta))));
}));
}
/**
* When detecting a ChannelClosed event, calls updateNonClosingBalanceProof with partner's balance
* proof, iff there's any
* TODO: do it only if economically viable (and define what that means)
*
* @param action$ - Observable of channelClose.success|newBlock actions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps members
* @returns Empty observable
*/
export function channelUpdateEpic(action$, state$, deps) {
const { log, signer, address, network, getTokenNetworkContract, config$ } = deps;
return action$.pipe(filter(isActionOf(channelClose.success)), filter((action) => !!action.payload.confirmed),
// wait a newBlock go through after channelClose confirmation, to ensure any pending
// channelSettle could have been processed
delayWhen(() => action$.pipe(filter(newBlock.is))), withLatestFrom(state$), filter(([action, state]) => {
const channel = state.channels[channelKey(action.meta)];
return (channel?.state === ChannelState.closed &&
channel.id === action.payload.id &&
channel.partner.balanceProof.transferredAmount
.add(channel.partner.balanceProof.lockedAmount)
.gt(Zero) && // there's partners balanceProof (i.e. received transfers)
channel.closeParticipant !== address // we're not the closing end
);
}), mergeMap(([action, state]) => {
const { tokenNetwork, partner } = action.meta;
const channel = state.channels[channelKey(action.meta)]; // checked in filter
const balanceHash = createBalanceHash(channel.partner.balanceProof);
const nonce = channel.partner.balanceProof.nonce;
const additionalHash = channel.partner.balanceProof.additionalHash;
const closingSignature = channel.partner.balanceProof.signature;
const nonClosingMessage = concatBytes([
encode(tokenNetwork, 20),
encode(network.chainId, 32),
encode(MessageTypeId.BALANCE_PROOF_UPDATE, 32),
encode(channel.id, 32),
encode(balanceHash, 32),
encode(nonce, 32),
encode(additionalHash, 32),
encode(closingSignature, 65), // partner's signature for this balance proof
]); // UInt8Array of 277 bytes
// send updateNonClosingBalanceProof transaction
return defer(() => signer.signMessage(nonClosingMessage)).pipe(mergeMap((nonClosingSignature) => transact(getTokenNetworkContract(tokenNetwork), 'updateNonClosingBalanceProof', [
channel.id,
partner,
address,
balanceHash,
nonce,
additionalHash,
closingSignature,
nonClosingSignature,
], deps, { error: ErrorCodes.CNL_UPDATE_NONCLOSING_BP_FAILED })), tap({
next: (v) => log.info('Updated channel', { channel, v }),
error: (error) => log.info('Error updating channel', { channel, error }),
}), retryWhile(intervalFromConfig(config$), {
onErrors: commonAndFailTxErrors,
log: log.info,
}),
// if succeeded, return a empty/completed observable
ignoreElements(), catchError((error) => {
log.error('Error updating non-closing balance-proof, ignoring', error);
return EMPTY;
}));
}));
}
//# sourceMappingURL=close.js.map