raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
865 lines • 48.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.transferGenerateAndSignEnvelopeMessageEpic = void 0;
const bignumber_1 = require("@ethersproject/bignumber");
const constants_1 = require("@ethersproject/constants");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const state_1 = require("../../channels/state");
const utils_1 = require("../../channels/utils");
const constants_2 = require("../../constants");
const actions_1 = require("../../messages/actions");
const types_1 = require("../../messages/types");
const utils_2 = require("../../messages/utils");
const actions_2 = require("../../transport/actions");
const utils_3 = require("../../transport/utils");
const utils_4 = require("../../utils");
const actions_3 = require("../../utils/actions");
const error_1 = require("../../utils/error");
const lru_1 = require("../../utils/lru");
const rx_1 = require("../../utils/rx");
const types_2 = require("../../utils/types");
const actions_4 = require("../actions");
const state_2 = require("../state");
const utils_5 = require("../utils");
const utils_6 = require("./utils");
// calculate locks array for channel end without lock with given secrethash
function withoutLock(end, secrethash) {
const locks = end.locks.filter((l) => l.secrethash !== secrethash);
(0, utils_4.assert)(locks.length === end.locks.length - 1, 'invalid locks size');
return locks;
}
// calculate locked amount for a given locks array
function totalLocked(locks) {
return locks.reduce((acc, { amount }) => acc.add(amount), constants_1.Zero);
}
// gets and asserts channel is open and optionally matches chain_id and channel_identifier
function getOpenChannel(state, key, matches) {
const channel = state.channels[(0, utils_1.channelKey)(key)];
(0, utils_4.assert)(channel?.state === state_1.ChannelState.open, 'channel not open');
if (matches) {
(0, utils_4.assert)(matches.chain_id.eq(state.chainId), 'chainId mismatch');
(0, utils_4.assert)(matches.channel_identifier.eq(channel.id), 'channelId mismatch');
}
return channel;
}
function transferRequestIsResolved(action) {
return action.payload.resolved;
}
/**
* The core logic of {@link makeAndSignTransfer}.
*
* @param state - Contains The current state of the app
* @param action - transfer request action to be sent.
* @param latest - Latest object
* @param latest.config - Config object
* @param latest.settleTimeout - contract's settleTimeout
* @param deps - {@link RaidenEpicDeps}
* @param deps.log - Logger instance
* @param deps.address - Our address
* @param deps.network - Current Network
* @param deps.signer - Signer instance
* @returns Observable of {@link transferSecret} or {@link transferSigned} actions
*/
function makeAndSignTransfer$(state, action, { config, settleTimeout }, { log, address, network, signer }) {
const { revealTimeout, expiryFactor } = config;
const { tokenNetwork, fee, partner, userId } = action.payload;
const channel = getOpenChannel(state, { tokenNetwork, partner });
const now = Math.round(Date.now() / 1e3); // now() in seconds (for messages and contracts)
let expiration;
if ('expiration' in action.payload && action.payload.expiration) {
expiration = action.payload.expiration; // reuse passed as is, in seconds
}
else {
expiration = now;
if ('lockTimeout' in action.payload && action.payload.lockTimeout)
expiration += action.payload.lockTimeout;
else
expiration += Math.min(revealTimeout * expiryFactor, settleTimeout - 1);
}
expiration = (0, types_2.decode)((0, types_2.UInt)(32), Math.ceil(expiration));
// revealTimeout < Δexpiration < settleTimeout
// to ensure we'll have enough time to tx in case it expires, but it can't outlive channel
(0, utils_4.assert)(expiration.gt(now + revealTimeout), [
'expiration too soon',
{ expiration, blockNumber: state.blockNumber, settleTimeout, revealTimeout },
]);
(0, utils_4.assert)(expiration.lt(now + settleTimeout), [
'expiration too far in the future',
{ expiration, blockNumber: state.blockNumber, settleTimeout, revealTimeout },
]);
const lock = {
// fee is added to the lock amount; overflow is checked on locksSum below
amount: action.payload.value.add(fee),
expiration,
secrethash: action.meta.secrethash,
};
const locks = [...channel.own.locks, lock];
const locksSum = totalLocked(locks);
(0, utils_4.assert)((0, types_2.UInt)(32).is(channel.own.balanceProof.transferredAmount.add(locksSum)), 'overflow on future transferredAmount');
const locksroot = (0, utils_5.getLocksroot)(locks);
log.info('Signing transfer of value', action.payload.value.toString(), 'of token', channel.token, ', to', action.payload.target, ', through routes', action.payload.metadata, ', paying', fee.toString(), 'in fees.');
const message = {
type: types_1.MessageType.LOCKED_TRANSFER,
message_identifier: (0, utils_5.makeMessageId)(),
chain_id: bignumber_1.BigNumber.from(network.chainId),
token_network_address: action.payload.tokenNetwork,
channel_identifier: bignumber_1.BigNumber.from(channel.id),
nonce: channel.own.nextNonce,
transferred_amount: channel.own.balanceProof.transferredAmount,
locked_amount: locksSum,
locksroot,
payment_identifier: action.payload.paymentId,
token: channel.token,
recipient: partner,
lock,
target: action.payload.target,
initiator: action.payload.initiator ?? address,
metadata: action.payload.metadata, // passthrough unchanged metadata
};
return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log })).pipe((0, operators_1.mergeMap)(function* (signed) {
// messageSend LockedTransfer handled by transferRetryMessageEpic
yield (0, actions_4.transferSigned)({ message: signed, fee, partner, userId }, action.meta);
// besides transferSigned, also yield transferSecret (for registering) if we know it
if (action.payload.secret)
yield (0, actions_4.transferSecret)({ secret: action.payload.secret }, action.meta);
}));
}
/**
* Create an observable to compose and sign a LockedTransfer message/transferSigned action
* As it's an async observable which depends on state and may return an action which changes it,
* the returned observable must be subscribed in a serialized context that ensures non-concurrent
* write access to the channel's balance proof (e.g. concatMap)
*
* @param state$ - Observable of current state
* @param action - transfer request action to be sent
* @param deps - RaidenEpicDeps
* @returns Observable of transferSigned|transferSecret|transfer.failure actions
*/
function sendTransferSigned(state$, action, deps) {
return (0, rxjs_1.combineLatest)([state$, deps.latest$]).pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(([state, latest]) => {
if (deps.db.storageKeys.has((0, utils_5.transferKey)(action.meta))) {
// don't throw to avoid emitting transfer.failure, to just wait for already pending transfer
deps.log.warn('transfer already present', action.meta);
return rxjs_1.EMPTY;
}
return makeAndSignTransfer$(state, action, latest, deps);
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.transfer.failure(err, action.meta))));
}
/**
* Contains the core logic of {@link makeAndSignUnlock}.
*
* @param state$ - Observable of the latest app state.
* @param state - Contains The current state of the app
* @param action - The transfer unlock action that will generate the transferUnlock.success action.
* @param transferState - Transfer's state
* @param deps - Epics dependencies
* @param deps.signer - The signer that will sign the message
* @param deps.log - Logger instance
* @param deps.db - Database instance
* @returns Observable of {@link transferUnlock.success} action.
*/
function makeAndSignUnlock$(state$, state, action, transferState, { log, signer, db }) {
const secrethash = action.meta.secrethash;
const locked = transferState.transfer;
const tokenNetwork = locked.token_network_address;
const partner = transferState.partner;
const channel = getOpenChannel(state, { tokenNetwork, partner });
let signed$;
if (transferState.unlock) {
// unlock already signed, use cached
signed$ = (0, rxjs_1.of)(transferState.unlock);
}
else {
(0, utils_4.assert)(transferState.secret, 'unknown secret'); // never fails because we wait before
(0, utils_4.assert)(channel.own.locks.find((lock) => lock.secrethash === secrethash), 'transfer already unlocked or expired');
// don't forget to check after signature too, may have expired by then
// allow unlocking past expiration if secret registered on-chain
(0, utils_4.assert)(transferState.secretRegistered || transferState.expiration >= Date.now() / 1e3, 'lock expired');
const message = {
type: types_1.MessageType.UNLOCK,
message_identifier: (0, utils_5.makeMessageId)(),
chain_id: locked.chain_id,
token_network_address: locked.token_network_address,
channel_identifier: locked.channel_identifier,
nonce: channel.own.nextNonce,
transferred_amount: (0, types_2.decode)((0, types_2.UInt)(32), channel.own.balanceProof.transferredAmount.add(locked.lock.amount), 'overflow on transferredAmount'),
locked_amount: channel.own.balanceProof.lockedAmount.sub(locked.lock.amount),
locksroot: (0, utils_5.getLocksroot)(withoutLock(channel.own, secrethash)),
payment_identifier: locked.payment_identifier,
secret: transferState.secret,
};
signed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log }));
}
return signed$.pipe((0, rx_1.withMergeFrom)(async () => (0, utils_5.getTransfer)(state$, db, action.meta)), (0, operators_1.withLatestFrom)(state$), (0, operators_1.map)(([[signed, transferState], state]) => {
const channel = getOpenChannel(state, { tokenNetwork, partner });
(0, utils_4.assert)(transferState &&
(transferState.expiration > Date.now() / 1e3 ||
transferState.secretRegistered ||
channel.own.locks.find((lock) => lock.secrethash === secrethash && lock.registered)), 'lock expired');
return actions_4.transferUnlock.success({ message: signed, partner, userId: action.payload?.userId }, action.meta);
// messageSend Unlock handled by transferRetryMessageEpic
// we don't check if transfer was refunded. If partner refunded the transfer but still
// forwarded the payment, we still act honestly and unlock if they revealed
}));
}
/**
* Create an observable to compose and sign a Unlock message/transferUnlock.success action
* As it's an async observable which depends on state and may return an action which changes it,
* the returned observable must be subscribed in a serialized context that ensures non-concurrent
* write access to the channel's balance proof (e.g. concatMap)
*
* @param state$ - Observable of current state
* @param action - transferUnlock.request request action to be sent
* @param deps - RaidenEpicDeps members
* @param deps.signer - Signer instance
* @param deps.log - Logger instance
* @param deps.db - Database instance
* @returns Observable of transferUnlock.success actions
*/
function sendTransferUnlocked(state$, action, deps) {
const { log, db } = deps;
return (0, rxjs_1.defer)(async () => (0, utils_5.getTransfer)(state$, db, action.meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => makeAndSignUnlock$(state$, state, action, transferState, deps)), (0, operators_1.catchError)((err) => {
log.warn('Error trying to unlock after SecretReveal', err);
return (0, rxjs_1.of)(actions_4.transferUnlock.failure(err, action.meta));
}));
}
/**
* Contains the core logic of {@link makeAndSignLockExpired}.
*
* @param state - Contains The current state of the app
* @param action - The transfer expire action.
* @param transferState - State of the transfer
* @param deps - RaidenEpicDeps members
* @param deps.signer - Signer instance
* @param deps.log - Logger instance
* @returns Observable of transferExpire.success actions
*/
function makeAndSignLockExpired$(state, action, transferState, { signer, log }) {
const secrethash = action.meta.secrethash;
const locked = transferState.transfer;
const tokenNetwork = locked.token_network_address;
const partner = transferState.partner;
const channel = getOpenChannel(state, { tokenNetwork, partner });
let signed$;
if (transferState.expired) {
// expired already signed, use cached
signed$ = (0, rxjs_1.of)(transferState.expired);
}
else {
(0, utils_4.assert)(locked.lock.expiration.lt(Math.floor(Date.now() / 1e3)), 'lock not yet expired');
(0, utils_4.assert)(channel.own.locks.find((lock) => lock.secrethash === secrethash) && !transferState.unlock, 'transfer already unlocked or expired');
const locksroot = (0, utils_5.getLocksroot)(withoutLock(channel.own, secrethash));
const message = {
type: types_1.MessageType.LOCK_EXPIRED,
message_identifier: (0, utils_5.makeMessageId)(),
chain_id: locked.chain_id,
token_network_address: locked.token_network_address,
channel_identifier: locked.channel_identifier,
nonce: channel.own.nextNonce,
transferred_amount: channel.own.balanceProof.transferredAmount,
locked_amount: channel.own.balanceProof.lockedAmount.sub(locked.lock.amount),
locksroot,
recipient: partner,
secrethash,
};
signed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log }));
}
return signed$.pipe(
// messageSend LockExpired handled by transferRetryMessageEpic
(0, operators_1.map)((signed) => actions_4.transferExpire.success({ message: signed, partner }, action.meta)));
}
/**
* Create an observable to compose and sign a LockExpired message/transferExpire.success action
* As it's an async observable which depends on state and may return an action which changes it,
* the returned observable must be subscribed in a serialized context that ensures non-concurrent
* write access to the channel's balance proof (e.g. concatMap)
*
* @param state$ - Observable of current state
* @param action - transfer request action to be sent
* @param deps - RaidenEpicDeps members
* @param deps.log - Logger instance
* @param deps.signer - Signer instance
* @param deps.db - Database instance
* @returns Observable of transferExpire.success|transferExpire.failure actions
*/
function sendTransferExpired(state$, action, { log, signer, db }) {
return (0, rxjs_1.defer)(() => (0, utils_5.getTransfer)(state$, db, action.meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => makeAndSignLockExpired$(state, action, transferState, { signer, log })), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.transferExpire.failure(err, action.meta))));
}
function receiveTransferSigned(state$, action, { address, log, signer, config$, db, latest$ }) {
const secrethash = action.payload.message.lock.secrethash;
const meta = { secrethash, direction: state_2.Direction.RECEIVED };
return (0, rxjs_1.combineLatest)([state$, config$, latest$]).pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(([state, { revealTimeout, caps }, { settleTimeout }]) => {
const locked = action.payload.message;
if (db.storageKeys.has((0, utils_5.transferKey)(meta))) {
log.warn('transfer already present', meta);
const msgId = locked.message_identifier;
const transferState = state.transfers[(0, utils_5.transferKey)(meta)];
// if transfer matches the stored one, re-send Processed once
if (transferState?.transferProcessed &&
transferState.partner === action.meta.address &&
msgId.eq(transferState.transfer.message_identifier)) {
// transferProcessed again will trigger messageSend.request
return (0, rxjs_1.of)((0, actions_4.transferProcessed)({ message: (0, types_2.untime)(transferState.transferProcessed), userId: action.payload.userId }, meta));
}
return rxjs_1.EMPTY;
}
// full balance proof validation
const tokenNetwork = locked.token_network_address;
const partner = action.meta.address;
const channel = getOpenChannel(state, { tokenNetwork, partner }, locked);
(0, utils_4.assert)(locked.nonce.eq(channel.partner.nextNonce), [
'nonce mismatch',
{ expected: channel.partner.nextNonce.toNumber(), received: locked.nonce.toNumber() },
]);
const locks = [...channel.partner.locks, locked.lock];
const locksroot = (0, utils_5.getLocksroot)(locks);
(0, utils_4.assert)(locked.locksroot === locksroot, 'locksroot mismatch');
(0, utils_4.assert)(locked.transferred_amount.eq(channel.partner.balanceProof.transferredAmount), 'transferredAmount mismatch');
const locksSum = totalLocked(locks);
(0, utils_4.assert)(locked.locked_amount.eq(locksSum), 'lockedAmount mismatch');
(0, utils_4.assert)((0, types_2.UInt)(32).is(channel.partner.balanceProof.transferredAmount.add(locksSum)), 'overflow on future transferredAmount');
const { partnerCapacity } = (0, utils_1.channelAmounts)(channel);
(0, utils_4.assert)(locked.lock.amount.lte(partnerCapacity), 'balanceProof total amount bigger than capacity');
(0, utils_4.assert)(locked.lock.expiration.lt(Math.ceil(Date.now() / 1e3 + settleTimeout)), [
'expiration too far in the future',
{
expiration: locked.lock.expiration,
blockNumber: state.blockNumber,
settleTimeout,
revealTimeout,
},
]);
// accept expired transfers, to apply state change and stay in sync
// with partner, so we can receive later LockExpired and transfers on top of it
(0, utils_4.assert)(locked.recipient === address, "Received transfer isn't for us");
log.info('Receiving transfer of value', locked.lock.amount.toString(), 'of token', channel.token, ', from', locked.initiator, ', through partner', partner);
let request$ = (0, rxjs_1.of)(undefined);
if (locked.target === address) {
let ignoredDetails;
// danger zone (our interval of confidence we can get secret register tx on-chain)
// starts revealTimeout millis before lock expiration (in secs)
const dangerZoneStart = locked.lock.expiration.sub(revealTimeout);
if (!(0, utils_3.getCap)(caps, constants_2.Capabilities.RECEIVE))
ignoredDetails = { reason: 'receiving disabled' };
else if (!dangerZoneStart.mul(1e3).gt(Date.now()))
ignoredDetails = {
reason: 'lock expired or expires too soon',
lockExpiration: locked.lock.expiration,
dangerZoneStart,
revealTimeout,
};
if (ignoredDetails) {
log.warn('Ignoring received transfer', ignoredDetails);
}
else {
request$ = (0, rxjs_1.defer)(async () => {
const decryptedSecret = (0, utils_5.decryptSecretFromMetadata)(locked.metadata, [secrethash, locked.lock.amount, locked.payment_identifier], signer);
if (decryptedSecret)
return decryptedSecret;
const request = {
type: types_1.MessageType.SECRET_REQUEST,
payment_identifier: locked.payment_identifier,
secrethash,
amount: locked.lock.amount,
expiration: locked.lock.expiration,
message_identifier: (0, utils_5.makeMessageId)(),
};
return (0, utils_2.signMessage)(signer, request, { log });
});
}
}
const processed$ = (0, rxjs_1.defer)(async () => {
const processed = {
type: types_1.MessageType.PROCESSED,
message_identifier: locked.message_identifier,
};
return (0, utils_2.signMessage)(signer, processed, { log });
});
// if any of these signature prompts fail, none of these actions will be emitted
return (0, rxjs_1.combineLatest)([processed$, request$]).pipe((0, operators_1.mergeMap)(function* ([processed, requestOrSecret]) {
yield (0, actions_4.transferSigned)({ message: locked, fee: constants_1.Zero, partner }, meta);
// sets TransferState.transferProcessed
yield (0, actions_4.transferProcessed)({ message: processed, userId: action.payload.userId }, meta);
if (types_2.Secret.is(requestOrSecret)) {
yield (0, actions_4.transferSecret)({ secret: requestOrSecret }, meta);
}
else if (requestOrSecret) {
// request initiator's presence, to be able to request secret
yield actions_2.matrixPresence.request(undefined, { address: locked.initiator });
// request secret iff we're the target and receiving is enabled
yield (0, actions_4.transferSecretRequest)({
message: requestOrSecret,
...(0, utils_5.searchValidViaAddress)(locked.metadata, locked.initiator),
}, meta);
}
}));
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.transfer.failure(err, meta))));
}
function receiveTransferUnlocked(state$, action, { log, signer, db }) {
const secrethash = (0, utils_5.getSecrethash)(action.payload.message.secret);
const meta = { secrethash, direction: state_2.Direction.RECEIVED };
// db.get will throw if not found, being handled on final catchError
return (0, rxjs_1.defer)(() => (0, utils_5.getTransfer)(state$, db, meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => {
(0, utils_4.assert)(transferState, 'unknown transfer');
const unlock = action.payload.message;
const partner = action.meta.address;
(0, utils_4.assert)(partner === transferState.partner, 'wrong partner');
// may race on db.get, but will validate on synchronous channel state reducer
(0, utils_4.assert)(!transferState.expired, 'already expired');
if (transferState.unlock) {
log.warn('transfer already unlocked', action.meta);
// if message matches the stored one, re-send Processed once
if (transferState.unlockProcessed &&
unlock.message_identifier.eq(transferState.unlockProcessed.message_identifier)) {
// transferProcessed again will trigger messageSend.request
return (0, rxjs_1.of)((0, actions_4.transferUnlockProcessed)({ message: (0, types_2.untime)(transferState.unlockProcessed), userId: action.payload.userId }, meta));
}
else
return rxjs_1.EMPTY;
}
const locked = transferState.transfer;
(0, utils_4.assert)(unlock.token_network_address === locked.token_network_address, 'wrong tokenNetwork');
// unlock validation
const tokenNetwork = unlock.token_network_address;
const channel = getOpenChannel(state, { tokenNetwork, partner }, unlock);
(0, utils_4.assert)(unlock.nonce.eq(channel.partner.nextNonce), [
'nonce mismatch',
{ expected: channel.partner.nextNonce.toNumber(), received: unlock.nonce.toNumber() },
]);
const amount = locked.lock.amount;
const locks = withoutLock(channel.partner, secrethash);
(0, utils_4.assert)(unlock.locksroot === (0, utils_5.getLocksroot)(locks), 'locksroot mismatch');
(0, utils_4.assert)(unlock.transferred_amount.eq(channel.partner.balanceProof.transferredAmount.add(amount)), 'transferredAmount mismatch');
(0, utils_4.assert)(unlock.locked_amount.eq(totalLocked(locks)), 'lockedAmount mismatch');
const processed = {
type: types_1.MessageType.PROCESSED,
message_identifier: unlock.message_identifier,
};
// if any of these signature prompts fail, none of these actions will be emitted
return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, processed, { log })).pipe((0, operators_1.mergeMap)(function* (processed) {
// we should already know the secret, but if not, persist again
if (!transferState.secret)
yield (0, actions_4.transferSecret)({ secret: unlock.secret }, meta);
yield actions_4.transferUnlock.success({ message: unlock, partner }, meta);
// sets TransferState.transferProcessed
yield (0, actions_4.transferUnlockProcessed)({ message: processed, userId: action.payload.userId }, meta);
yield actions_4.transfer.success({ balanceProof: (0, utils_2.getBalanceProofFromEnvelopeMessage)(unlock) }, meta);
}));
}), (0, operators_1.catchError)((err) => {
log.warn('Error trying to process received Unlock', err);
return (0, rxjs_1.of)(actions_4.transferUnlock.failure(err, meta));
}));
}
function receiveTransferExpired(state$, action, { log, signer, db }) {
const secrethash = action.payload.message.secrethash;
const meta = { secrethash, direction: state_2.Direction.RECEIVED };
// db.get will throw if not found, being handled on final catchError
return (0, rxjs_1.defer)(() => (0, utils_5.getTransfer)(state$, db, meta)).pipe((0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([transferState, state]) => {
const expired = action.payload.message;
const partner = action.meta.address;
(0, utils_4.assert)(partner === transferState.partner, 'wrong partner');
// may race on db.get, but will validate on synchronous channel state reducer
(0, utils_4.assert)(!transferState.unlock, 'transfer unlocked');
if (transferState.expired) {
log.warn('transfer already expired', action.meta);
// if message matches the stored one, re-send Processed once
if (transferState.expiredProcessed &&
expired.message_identifier.eq(transferState.expiredProcessed.message_identifier)) {
// transferProcessed again will trigger messageSend.request
return (0, rxjs_1.of)((0, actions_4.transferExpireProcessed)({ message: (0, types_2.untime)(transferState.expiredProcessed), userId: action.payload.userId }, meta));
}
else
return rxjs_1.EMPTY;
}
const locked = transferState.transfer;
// expired validation
(0, utils_4.assert)(locked.lock.expiration.lt(Math.ceil(Date.now() / 1e3)), 'not expired yet');
(0, utils_4.assert)(!transferState.secretRegistered, 'secret registered onchain');
(0, utils_4.assert)(expired.token_network_address === locked.token_network_address, 'wrong tokenNetwork');
const tokenNetwork = expired.token_network_address;
const channel = getOpenChannel(state, { tokenNetwork, partner }, expired);
(0, utils_4.assert)(expired.nonce.eq(channel.partner.nextNonce), [
'nonce mismatch',
{ expected: channel.partner.nextNonce.toNumber(), received: expired.nonce.toNumber() },
]);
const locks = withoutLock(channel.partner, secrethash);
(0, utils_4.assert)(expired.locksroot === (0, utils_5.getLocksroot)(locks), 'locksroot mismatch');
(0, utils_4.assert)(expired.locked_amount.eq(totalLocked(locks)), 'lockedAmount mismatch');
(0, utils_4.assert)(expired.transferred_amount.eq(channel.partner.balanceProof.transferredAmount), 'transferredAmount mismatch');
const processed = {
type: types_1.MessageType.PROCESSED,
message_identifier: expired.message_identifier,
};
// if any of these signature prompts fail, none of these actions will be emitted
return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, processed, { log })).pipe((0, operators_1.mergeMap)(function* (processed) {
yield actions_4.transferExpire.success({ message: expired, partner }, meta);
// sets TransferState.transferProcessed
yield (0, actions_4.transferExpireProcessed)({ message: processed, userId: action.payload.userId }, meta);
yield actions_4.transfer.failure(new error_1.RaidenError(error_1.ErrorCodes.XFER_EXPIRED, {
block: locked.lock.expiration.toString(),
}), meta);
}));
}), (0, operators_1.catchError)((err) => {
log.warn('Error trying to process received LockExpired', err);
return (0, rxjs_1.of)(actions_4.transferExpire.failure(err, meta));
}));
}
function isWithdrawConfirmation(m) {
return m.type === types_1.MessageType.WITHDRAW_CONFIRMATION;
}
/**
* Handles a withdraw.request and send a WithdrawRequest to partner
*
* @param state$ - Observable of current state
* @param action - Withdraw request which caused this handling
* @param deps - RaidenEpicDeps members
* @param deps.address - Our address
* @param deps.log - Logger instance
* @param deps.signer - Signer instance
* @param deps.network - Current Network
* @param deps.config$ - Config observable
* @returns Observable of withdrawMessage.request|withdraw.failure actions
*/
function sendWithdrawRequest(state$, action, { log, address, signer, network, config$ }) {
if (action.meta.direction !== state_2.Direction.SENT)
return rxjs_1.EMPTY;
return (0, rxjs_1.combineLatest)([state$, config$]).pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(([state, { revealTimeout }]) => {
const channel = getOpenChannel(state, action.meta);
if (channel.own.pendingWithdraws.some((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_REQUEST, action.meta)))
return rxjs_1.EMPTY; // already requested, skip without failing
// next assert prevents parallel requests from being performed, but we special case here:
// in case there's an still valid confirmation which failed for any reason, we emit it again
// in order to retry sending the respective transaction
const oldConfirmation = channel.own.pendingWithdraws.find(isWithdrawConfirmation);
if (oldConfirmation) {
return [
actions_4.withdrawMessage.success({ message: oldConfirmation }, {
direction: action.meta.direction,
tokenNetwork: channel.tokenNetwork,
partner: channel.partner.address,
totalWithdraw: oldConfirmation.total_withdraw,
expiration: oldConfirmation.expiration.toNumber(),
}),
actions_4.withdraw.failure(new error_1.RaidenError(error_1.ErrorCodes.CNL_WITHDRAW_RETRY_CONFIRMATION), action.meta),
];
}
// although it'd be possible parallel withdraw requests per protocol, PC doesn't like it and
// will get out of sync if we try, so we must prevent it
(0, utils_4.assert)(!channel.own.pendingWithdraws.length, [
error_1.ErrorCodes.CNL_WITHDRAW_PENDING,
{ pendingWithdraws: channel.own.pendingWithdraws },
]);
(0, utils_4.assert)(action.meta.totalWithdraw.lte((0, utils_1.channelAmounts)(channel).ownTotalWithdrawable), error_1.ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_HIGH);
if (action.payload?.coopSettle !== undefined) {
// coopSettle reply has a more relaxed constraint on expiration
(0, utils_4.assert)(action.meta.expiration > Date.now() / 1e3, error_1.ErrorCodes.CNL_WITHDRAW_EXPIRES_SOON);
const { ownLocked, partnerLocked, ownTotalWithdrawable, partnerCapacity } = (0, utils_1.channelAmounts)(channel);
(0, utils_4.assert)(!channel.own.locks.length &&
!channel.partner.locks.length &&
action.meta.totalWithdraw.eq(ownTotalWithdrawable) &&
(action.payload.coopSettle || partnerCapacity.isZero()), [error_1.ErrorCodes.CNL_COOP_SETTLE_NOT_POSSIBLE, { ownLocked, partnerLocked, partnerCapacity }]);
}
else {
(0, utils_4.assert)(action.meta.totalWithdraw.gt(channel.own.withdraw), error_1.ErrorCodes.CNL_WITHDRAW_AMOUNT_TOO_LOW);
(0, utils_4.assert)(action.meta.expiration >= Date.now() / 1e3 + revealTimeout, error_1.ErrorCodes.CNL_WITHDRAW_EXPIRES_SOON);
}
const request = {
type: types_1.MessageType.WITHDRAW_REQUEST,
message_identifier: (0, utils_5.makeMessageId)(),
chain_id: bignumber_1.BigNumber.from(network.chainId),
token_network_address: action.meta.tokenNetwork,
channel_identifier: bignumber_1.BigNumber.from(channel.id),
participant: address,
total_withdraw: action.meta.totalWithdraw,
nonce: channel.own.nextNonce,
expiration: bignumber_1.BigNumber.from(action.meta.expiration),
...(action.payload?.coopSettle !== undefined
? { coop_settle: action.payload.coopSettle }
: null),
};
return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, request, { log })).pipe((0, operators_1.map)((message) => actions_4.withdrawMessage.request({ message }, action.meta)));
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdraw.failure(err, action.meta))));
}
/**
* Validates a received WithdrawConfirmation message
*
* @param state$ - Observable of current state
* @param action - Withdraw request which caused this handling
* @param deps - RaidenEpicDeps members
* @param deps.address - Our address
* @returns Observable of withdrawMessage.success actions
*/
function receiveWithdrawConfirmation(state$, action, { address }) {
const confirmation = action.payload.message;
const tokenNetwork = confirmation.token_network_address;
const partner = action.meta.address;
let expiration;
try {
expiration = confirmation.expiration.toNumber(); // expiration could be too big
}
catch (e) {
return rxjs_1.EMPTY;
}
const withdrawMeta = {
direction: state_2.Direction.SENT,
tokenNetwork,
partner,
totalWithdraw: confirmation.total_withdraw,
expiration,
};
return state$.pipe((0, operators_1.first)(), (0, operators_1.map)((state) => {
getOpenChannel(state, { tokenNetwork, partner }, confirmation);
(0, utils_4.assert)(confirmation.participant === address, 'participant mismatch');
// don't validate request presence here, to always update nonce, but do on tx send
return actions_4.withdrawMessage.success({ message: confirmation }, withdrawMeta);
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawMessage.failure(err, withdrawMeta))));
}
/**
* Handles a withdrawExpire.request, sign and send a WithdrawExpired message to partner
*
* @param state$ - Observable of current state
* @param action - WithdrawExpire request which caused this call
* @param deps - RaidenEpicDeps members
* @param deps.address - Our address
* @param deps.log - Logger instance
* @param deps.signer - Signer instance
* @returns Observable of withdrawMessage.request|withdraw.failure actions
*/
function sendWithdrawExpired(state$, action, { log, address, signer }) {
if (action.meta.direction !== state_2.Direction.SENT)
return rxjs_1.EMPTY;
return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)((state) => {
const channel = getOpenChannel(state, action.meta);
(0, utils_4.assert)(!channel.own.pendingWithdraws.some((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_EXPIRED, action.meta)), 'already expired');
const req = channel.own.pendingWithdraws.find((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_REQUEST, action.meta));
(0, utils_4.assert)(req, 'no matching WithdrawRequest found, maybe already confirmed');
(0, utils_4.assert)(action.meta.expiration < Date.now() / 1e3, 'not expired yet');
const expired = {
type: types_1.MessageType.WITHDRAW_EXPIRED,
message_identifier: (0, utils_5.makeMessageId)(),
chain_id: req.chain_id,
token_network_address: req.token_network_address,
channel_identifier: req.channel_identifier,
participant: address,
total_withdraw: action.meta.totalWithdraw,
nonce: channel.own.nextNonce,
expiration: req.expiration,
};
return (0, rxjs_1.from)((0, utils_2.signMessage)(signer, expired, { log })).pipe((0, operators_1.map)((message) => actions_4.withdrawExpire.success({ message }, action.meta)));
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawExpire.failure(err, action.meta))));
}
/**
* Validate a [[WithdrawRequest]], compose and send a [[WithdrawConfirmation]]
*
* Validate channel exists and is open, and that total_withdraw is less than or equal withdrawable
* amount. We need it inside [[transferGenerateAndSignEnvelopeMessageEpic]] concatMap/lock because
* we read and change 'nextNonce', even though WithdrawConfirmation doesn't carry a full
* balanceProof. If request's nonce is valid but it already expired (old replied message), we still
* accept the state change, but don't compose/send the confirmation, and let it expire.
* Instead of storing confirmation in state and retrying, we just cache it and send the cached
* signed message on each retried request received.
*
* @param state$ - Observable of current state
* @param action - Withdraw request which caused this handling
* @param signer - RaidenEpicDeps members
* @param signer.signer - Signer instance
* @param signer.log - Logger instance
* @returns Observable of transferExpire.success|transferExpire.failure actions
*/
function receiveWithdrawRequest(state$, action, { signer, log }) {
const request = action.payload.message;
const tokenNetwork = request.token_network_address;
const partner = request.participant;
let expiration;
try {
expiration = request.expiration.toNumber(); // expiration could be too big
}
catch (e) {
return rxjs_1.EMPTY;
}
const withdrawMeta = {
direction: state_2.Direction.RECEIVED,
tokenNetwork,
partner,
totalWithdraw: request.total_withdraw,
expiration,
};
return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)((state) => {
(0, utils_4.assert)(request.participant === action.meta.address, 'participant mismatch');
const channel = getOpenChannel(state, { tokenNetwork, partner }, request);
let confirmation$;
const persistedConfirmation = channel.partner.pendingWithdraws.find((0, utils_6.matchWithdraw)(types_1.MessageType.WITHDRAW_CONFIRMATION, request));
if (persistedConfirmation) {
confirmation$ = (0, rxjs_1.of)(persistedConfirmation);
}
else {
(0, utils_4.assert)(request.nonce.eq(channel.partner.nextNonce), [
'nonce mismatch',
{ expected: channel.partner.nextNonce.toNumber(), received: request.nonce.toNumber() },
]);
(0, utils_4.assert)(request.total_withdraw.lte((0, utils_1.channelAmounts)(channel).partnerTotalWithdrawable), 'invalid total_withdraw');
// don't mind expiration, and always reply with confirmation
// expired confirmations are useless on-chain, but confirms message
const confirmation = {
type: types_1.MessageType.WITHDRAW_CONFIRMATION,
message_identifier: request.message_identifier,
chain_id: request.chain_id,
token_network_address: request.token_network_address,
channel_identifier: request.channel_identifier,
participant: request.participant,
total_withdraw: request.total_withdraw,
nonce: channel.own.nextNonce,
expiration: request.expiration,
};
confirmation$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, confirmation, { log }));
}
return confirmation$.pipe((0, operators_1.mergeMap)(function* (message) {
// first, emit 'WithdrawRequest', to increase partner's nonce in state
yield actions_4.withdrawMessage.request({ message: request }, withdrawMeta);
// emit our composed 'WithdrawConfirmation' to increase our nonce in state
yield actions_4.withdrawMessage.success({ message }, withdrawMeta);
// send once per received request; confirmation signature is cached above
yield actions_1.messageSend.request({ message }, {
address: partner,
msgId: action.payload.message.message_identifier.toString(),
});
}));
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawMessage.failure(err, withdrawMeta))));
}
/**
* Validate a received [[WithdrawExpired]] message, emit withdrawExpire.success and send Processed
*
* @param state$ - Observable of current state
* @param action - Withdraw request which caused this handling
* @param signer - RaidenEpicDeps members
* @param signer.signer - Signer instance
* @param signer.log - Logger instance
* @param cache - A Map to store and reuse previously Signed<WithdrawConfirmation>
* @returns Observable of withdrawExpire.success|withdrawExpireProcessed|messageSend.request actions
*/
function receiveWithdrawExpired(state$, action, { signer, log }, cache) {
const expired = action.payload.message;
const tokenNetwork = expired.token_network_address;
const partner = expired.participant;
let expiration;
try {
expiration = expired.expiration.toNumber(); // expiration could be too big
}
catch (e) {
return rxjs_1.EMPTY;
}
const withdrawMeta = {
direction: state_2.Direction.RECEIVED,
tokenNetwork,
partner,
totalWithdraw: expired.total_withdraw,
expiration,
};
return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)((state) => {
(0, utils_4.assert)(partner === action.meta.address, 'participant mismatch');
const channel = getOpenChannel(state, { tokenNetwork, partner }, expired);
let processed$;
const cacheKey = `${(0, utils_1.channelUniqueKey)(channel)}+${expired.message_identifier.toString()}`;
const cached = cache.get(cacheKey);
if (cached)
processed$ = (0, rxjs_1.of)(cached);
else {
(0, utils_4.assert)(expired.nonce.lte(channel.partner.nextNonce), [
'nonce mismatch',
{ expected: channel.partner.nextNonce.toNumber(), received: expired.nonce.toNumber() },
]);
(0, utils_4.assert)(withdrawMeta.expiration <= Date.now() / 1e3, 'not expired yet');
const processed = {
type: types_1.MessageType.PROCESSED,
message_identifier: expired.message_identifier,
};
processed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, processed, { log })).pipe((0, operators_1.tap)((signed) => cache.set(cacheKey, signed)));
}
return processed$.pipe((0, operators_1.mergeMap)(function* (processed) {
// as we've received and validated this message, emit failure to increment nextNonce
yield actions_4.withdrawExpire.success({ message: expired }, withdrawMeta);
// emits withdrawCompleted to clear messages from partner's pendingWithdraws array
yield actions_1.messageSend.request({ message: processed }, { address: partner, msgId: processed.message_identifier.toString() });
yield (0, actions_4.withdrawCompleted)(undefined, withdrawMeta);
}));
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_4.withdrawExpire.failure(err, withdrawMeta))));
}
/**
* Serialize creation and signing of BalanceProof-changing messages or actions
* Actions which change any data in any channel balance proof must only ever be created reading
* state inside the serialization flow provided by the concatMap, and also be composed and produced
* inside it (inner, subscribed observable)
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps
* @returns Observable of output actions for this epic
*/
function transferGenerateAndSignEnvelopeMessageEpic(action$, {}, deps) {
const processedCache = new lru_1.LruCache(32);
const state$ = deps.latest$.pipe((0, rx_1.pluckDistinct)('state'), (0, rx_1.completeWith)(action$)); // replayed(1)' state$
return (0, rxjs_1.merge)(action$.pipe((0, operators_1.filter)((0, actions_3.isActionOf)([
actions_4.transfer.request,
actions_4.transferUnlock.request,
actions_4.transferExpire.request,
actions_4.withdraw.request,
actions_4.withdrawExpire.request,
]))),
// merge separatedly, to filter per message type before concat
action$.pipe((0, operators_1.filter)((0, utils_2.isMessageReceivedOfType)([
(0, types_2.Signed)(types_1.LockedTransfer),
(0, types_2.Signed)(types_1.Unlock),
(0, types_2.Signed)(types_1.LockExpired),
(0, types_2.Signed)(types_1.WithdrawRequest),
(0, types_2.Signed)(types_1.WithdrawConfirmation),
(0, types_2.Signed)(types_1.WithdrawExpired),
])))).pipe((0, operators_1.concatMap)((action) => {
let output$;
switch (action.type) {
case actions_4.transfer.request.type:
if (transferRequestIsResolved(action))
output$ = sendTransferSigned(state$, action, deps);
else
output$ = rxjs_1.EMPTY;
break;
case actions_4.transferUnlock.request.type:
output$ = sendTransferUnlocked(state$, action, deps);
break;
case actions_4.transferExpire.request.type:
output$ = sendTransferExpired(state$, action, deps);
break;
case actions_4.withdraw.request.type:
output$ = sendWithdrawRequest(state$, action, deps);
break;
case actions_4.withdrawExpire.request.type:
output$ = sendWithdrawExpired(state$, action, deps);
break;
case actions_1.messageReceived.type:
switch (action.payload.message.type) {
case types_1.MessageType.LOCKED_TRANSFER:
output$ = receiveTransferSigned(state$, action, deps);
break;
case types_1.MessageType.UNLOCK:
output$ = receiveTransferUnlocked(state$, action, deps);
break;
case types_1.MessageType.LOCK_EXPIRED:
output$ = receiveTransferExpired(state$, action, deps);
break;
case types_1.MessageType.WITHDRAW_REQUEST:
output$ = receiveWithdrawRequest(state$, action, deps);
break;
case types_1.MessageType.WITHDRAW_CONFIRMATION:
output$ = receiveWithdrawConfirmation(state$, action, deps);
break;
case types_1.MessageType.WITHDRAW_EXPIRED:
output$ = receiveWithdrawExpired(state$, action, deps, processedCache);
break;
}
break;
}
return output$;
}));
}
exports.transferGenerateAndSignEnvelopeMessageEpic = transferGenerateAndSignEnvelopeMessageEpic;
//# sourceMappingURL=locked.js.map