raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
251 lines • 10.9 kB
JavaScript
import { AddressZero, One, Zero } from '@ethersproject/constants';
import { ConfirmableAction } from '../actions';
import { initialState } from '../state';
import { transferSecretRegister } from '../transfers/actions';
import { Direction } from '../transfers/state';
import { createReducer } from '../utils/actions';
import { partialCombineReducers } from '../utils/redux';
import { channelClose, channelDeposit, channelOpen, channelSettle, channelSettleable, channelWithdrawn, newBlock, tokenMonitored, } from './actions';
import { ChannelState } from './state';
import { BalanceProofZero } from './types';
import { channelKey, channelUniqueKey } from './utils';
// state.blockNumber specific reducer, handles only newBlock action
const blockNumber = createReducer(initialState.blockNumber).handle(newBlock, ({}, { payload }) => payload.blockNumber);
// state.tokens specific reducer, handles only tokenMonitored action
const tokens = createReducer(initialState.tokens).handle(tokenMonitored, (state, { payload: { token, tokenNetwork } }) => state[token] === tokenNetwork ? state : { ...state, [token]: tokenNetwork });
function removeAction(pendingTxs, action) {
return pendingTxs.filter((a) => a.type !== action.type || action.payload.txHash !== a.payload.txHash);
}
const pendingTxs = (state = initialState.pendingTxs, action) => {
// filter out non-ConfirmableActions's
if (!ConfirmableAction.is(action))
return state;
// if confirmed==undefined, deduplicate and add action to state
else if (action.payload.confirmed === undefined)
return [...removeAction(state, action), action];
// else (either confirmed or removed), remove from state
else {
const newState = removeAction(state, action);
if (newState.length !== state.length)
return newState;
return state;
}
};
const emptyChannelEnd = {
address: AddressZero,
deposit: Zero,
withdraw: Zero,
locks: [],
balanceProof: BalanceProofZero,
pendingWithdraws: [],
nextNonce: One,
};
function channelOpenSuccessReducer(state, action) {
const key = channelKey(action.meta);
// ignore if older than currently set channel, or unconfirmed or removed
const prevChannel = state.channels[key];
if ((prevChannel?.openBlock ?? 0) >= action.payload.txBlock ||
(prevChannel?.id ?? 0) >= action.payload.id ||
!action.payload.confirmed)
return state;
const channel = {
_id: channelUniqueKey({ ...action.meta, id: action.payload.id }),
id: action.payload.id,
state: ChannelState.open,
token: action.payload.token,
tokenNetwork: action.meta.tokenNetwork,
isFirstParticipant: action.payload.isFirstParticipant,
openBlock: action.payload.txBlock,
own: {
...emptyChannelEnd,
address: state.address,
},
partner: {
...emptyChannelEnd,
address: action.meta.partner,
},
};
return { ...state, channels: { ...state.channels, [key]: channel } };
}
function channelUpdateOnchainBalanceStateReducer(state, action) {
// ignore event if unconfirmed or removed
if (!action.payload.confirmed)
return state;
const key = channelKey(action.meta);
let channel = state.channels[key];
if (channel?.state !== ChannelState.open || channel.id !== action.payload.id)
return state;
const end = action.payload.participant === channel.partner.address ? 'partner' : 'own';
const [prop, total, pendingWithdraws] = channelWithdrawn.is(action)
? [
'withdraw',
action.payload.totalWithdraw,
channel[end].pendingWithdraws.filter((req) => req.total_withdraw.gt(action.payload.totalWithdraw)), // on-chain withdraw clears <= withdraw messages, including the confirmed one
]
: ['deposit', action.payload.totalDeposit, channel[end].pendingWithdraws];
if (total.lte(channel[end][prop]))
return state; // ignore if past event
channel = {
...channel,
[end]: {
...channel[end],
[prop]: total,
pendingWithdraws,
},
};
return { ...state, channels: { ...state.channels, [key]: channel } };
}
function channelCloseSuccessReducer(state, action) {
const key = channelKey(action.meta);
let channel = state.channels[key];
if (channel?.id !== action.payload.id)
return state;
// even on non-confirmed action, already set channel state as closing, so it can't be used for new transfers
if (action.payload.confirmed === undefined && channel.state === ChannelState.open)
channel = { ...channel, state: ChannelState.closing };
else if (!('closeBlock' in channel) && action.payload.confirmed)
channel = {
...channel,
state: ChannelState.closed,
closeBlock: action.payload.txBlock,
closeParticipant: action.payload.participant,
};
else
return state;
return { ...state, channels: { ...state.channels, [key]: channel } };
}
function channelUpdateStateReducer(state, action) {
const key = channelKey(action.meta);
let channel = state.channels[key];
if (!channel)
return state;
if (channelClose.request.is(action) && channel.state === ChannelState.open) {
channel = { ...channel, state: ChannelState.closing };
}
else if (channelClose.failure.is(action) && channel.state === ChannelState.closing) {
channel = { ...channel, state: ChannelState.open }; // rollback to open
}
else if (channelSettle.request.is(action) && channel.state === ChannelState.settleable) {
channel = { ...channel, state: ChannelState.settling };
}
else if (channelSettleable.is(action) && channel.state === ChannelState.closed) {
channel = { ...channel, state: ChannelState.settleable };
}
else {
return state;
}
return { ...state, channels: { ...state.channels, [key]: channel } };
}
function channelSettleSuccessReducer(state, action) {
const key = channelKey(action.meta);
const channel = state.channels[key];
if (channel?.id !== action.payload.id)
return state;
// closeBlock,closeParticipant get defaults if channel was coop-settled
const closeInfo = {
closeBlock: 'closeBlock' in channel ? channel.closeBlock : 0,
closeParticipant: 'closeParticipant' in channel ? channel.closeParticipant : AddressZero,
};
if (action.payload.confirmed === undefined && channel.state !== ChannelState.settling)
// on non-confirmed action, set channel as settling
return {
...state,
channels: {
...state.channels,
[key]: {
...channel,
...closeInfo,
state: ChannelState.settling,
},
},
};
else if (action.payload.confirmed) {
const { [key]: _, ...channels } = state.channels; // pop [key] channel out of state.channels
return {
...state,
channels,
oldChannels: {
// persist popped channel on oldChannels with augmented channelKey
...state.oldChannels,
[channelUniqueKey(channel)]: {
...channel,
...closeInfo,
state: ChannelState.settled,
settleBlock: action.payload.txBlock,
},
},
};
}
return state;
}
/* Immutably mark locks as registed; returns reference to previous array if nothing changes */
function markLocksAsRegistered(locks, secrethash, registeredTimestamp) {
let changed = false;
const newLocks = locks.map((lock) => {
if (lock.secrethash !== secrethash ||
lock.registered ||
lock.expiration.lte(registeredTimestamp))
return lock;
changed = true;
return { ...lock, registered: true };
});
if (changed)
return newLocks;
return locks;
}
function channelLockRegisteredReducer(state, action) {
// now that secret is stored in transfer, if it's a confirmed on-chain registration,
// also update channel's lock to reflect it
if (!action.payload.confirmed)
return state;
const end = action.meta.direction === Direction.SENT ? 'own' : 'partner';
// iterate over channels and update any matching lock
for (const [key, channel] of Object.entries(state.channels)) {
const newLocks = markLocksAsRegistered(channel[end].locks, action.meta.secrethash, action.payload.txTimestamp);
if (newLocks === channel[end].locks)
continue;
// only update state if locks changed
state = {
...state,
channels: {
...state.channels,
[key]: {
...channel,
[end]: {
...channel[end],
locks: newLocks,
},
},
},
};
}
return state;
}
// handles actions which reducers need RaidenState
const completeReducer = createReducer(initialState)
.handle(channelOpen.success, channelOpenSuccessReducer)
.handle([channelDeposit.success, channelWithdrawn], channelUpdateOnchainBalanceStateReducer)
.handle([channelClose.request, channelClose.failure, channelSettleable, channelSettle.request], channelUpdateStateReducer)
.handle(channelClose.success, channelCloseSuccessReducer)
.handle(channelSettle.success, channelSettleSuccessReducer)
.handle(transferSecretRegister.success, channelLockRegisteredReducer);
/**
* Nested/combined reducer for channels
* blockNumber, tokens & pendingTxs reducers get its own slice of the state, corresponding to the
* name of the reducer. channels root reducer instead must be handled the complete state instead,
* so it compose the output with each key/nested/combined state.
*/
const partialReducer = partialCombineReducers({ blockNumber, tokens, pendingTxs }, initialState);
/**
* channelsReducer is a reduce-reducers like reducer; in contract with combineReducers, which
* gives just a specific slice of the state to the reducer (like blockNumber above, which receives
* only blockNumber), it actually act as a normal reducer by getting the whole state, but can do
* it over several reducers, passing the output of one as the input for next
*
* @param state - previous root RaidenState
* @param action - RaidenAction to try to handle
* @returns - new RaidenState
*/
const channelsReducer = (state = initialState, action) => [partialReducer, completeReducer].reduce((s, reducer) => reducer(s, action), state);
export default channelsReducer;
//# sourceMappingURL=reducer.js.map