raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
301 lines • 18.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.transferSecretRegisterEpic = exports.transferAutoRegisterEpic = exports.transferSuccessOnSecretRegisteredEpic = exports.monitorSecretRegistryEpic = exports.transferRequestUnlockEpic = exports.transferSecretRevealedEpic = exports.transferSecretRevealEpic = exports.transferSecretRequestedEpic = void 0;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const utils_1 = require("../../channels/utils");
const config_1 = require("../../config");
const constants_1 = require("../../constants");
const actions_1 = require("../../messages/actions");
const types_1 = require("../../messages/types");
const utils_2 = require("../../messages/utils");
const utils_3 = require("../../transport/utils");
const actions_2 = require("../../utils/actions");
const error_1 = require("../../utils/error");
const ethers_1 = require("../../utils/ethers");
const rx_1 = require("../../utils/rx");
const types_2 = require("../../utils/types");
const actions_3 = require("../actions");
const state_1 = require("../state");
const utils_4 = require("../utils");
const utils_5 = require("./utils");
/**
* Handles receiving a signed SecretRequest from target for some sent LockedTransfer
* Emits a transferSecretRequest action only if all conditions are met
*
* @param action$ - Observable of messageReceived actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.address - Our address
* @param deps.log - Logger instance
* @returns Observable of transferSecretRequest actions
*/
function transferSecretRequestedEpic(action$, state$, { address, log }) {
return action$.pipe((0, operators_1.filter)((0, utils_2.isMessageReceivedOfType)((0, types_2.Signed)(types_1.SecretRequest))), (0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(function* ([action, state]) {
const message = action.payload.message;
// proceed only if we know the secret and the SENT transfer
const key = (0, utils_4.transferKey)({ secrethash: message.secrethash, direction: state_1.Direction.SENT });
const transferState = state.transfers[key];
if (!transferState)
return;
const locked = transferState.transfer;
// we do only some basic verification here, as most of it is done upon SecretReveal,
// to persist the request in most cases in TransferState.secretRequest
if (locked.initiator !== address || // only the initiator may reply a SecretRequest
locked.target !== action.meta.address || // reveal only to target
!locked.payment_identifier.eq(message.payment_identifier)) {
log.warn('Invalid SecretRequest for transfer', message, locked);
return;
}
yield (0, actions_3.transferSecretRequest)({ message, userId: action.payload.userId }, { secrethash: message.secrethash, direction: state_1.Direction.SENT });
}));
}
exports.transferSecretRequestedEpic = transferSecretRequestedEpic;
/**
* Contains the core logic of {@link transferSecretRevealEpic}.
*
* @param action - The {@link transferSecretRequest} action that
* @param deps - Epics dependencies
* @param deps.signer - Signer instance
* @param deps.log - Logger instance
* @param deps.latest$ - Latest observable
* @returns Observable of {@link transfer.failure}, {@link transferSecretReveal} or
* {@link messageSend.request} actions
*/
const secretReveal$ = (action, { signer, log, latest$ }) => {
const request = action.payload.message;
return latest$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(({ state }) => {
const transferState = state.transfers[(0, utils_4.transferKey)(action.meta)];
// shouldn't happen, as we're the initiator (for now), and always know the secret
(0, error_1.assert)(transferState?.secret, ['SecretRequest for unknown secret', request]);
const locked = transferState.transfer;
const target = locked.target;
const fee = transferState.fee;
const value = locked.lock.amount.sub(fee);
(0, error_1.assert)(request.expiration.lte(locked.lock.expiration) &&
request.expiration.gt(Math.floor(Date.now() / 1e3)), ['SecretRequest for expired transfer', { request, lock: locked.lock }]);
(0, error_1.assert)(request.amount.gte(value), [
'SecretRequest for amount too small!',
{ request, value },
]);
if (!request.amount.eq(value)) {
// accept request
log.info('Accepted SecretRequest for amount greater than sent', request, locked);
}
let reveal$;
if (transferState.secretReveal)
reveal$ = (0, rxjs_1.of)(transferState.secretReveal);
else {
const message = {
type: types_1.MessageType.SECRET_REVEAL,
message_identifier: (0, utils_4.makeMessageId)(),
secret: transferState.secret,
};
reveal$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log }));
}
return reveal$.pipe((0, operators_1.mergeMap)(function* (message) {
yield (0, actions_3.transferSecretReveal)({ message }, action.meta);
yield actions_1.messageSend.request({ message, userId: action.payload.userId }, { address: target, msgId: message.message_identifier.toString() });
}));
}), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_3.transfer.failure(err, action.meta))));
};
/**
* Handles a transferSecretRequest action to send the respective secret to target
* It both emits transferSecretReveal (to persist sent SecretReveal in state and indicate that
* the secret was revealed and thus the transfer should be assumed as succeeded) as well as
* triggers sending the message once. New SecretRequests will cause a new transferSecretRequest,
* which will re-send the cached SecretReveal.
* transfer.failure is emitted in case invalid secretRequest comes, as no valid one will come as
* per current implementation, so we fail early to notify users about it.
*
* @param action$ - Observable of transferSecretRequest actions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps
* @param deps.signer - Signer instance
* @param deps.latest$ - Latest observable
* @param deps.log - Logger instance
* @returns Observable of transfer.failure|transferSecretReveal|messageSend.request actions
*/
function transferSecretRevealEpic(action$, {}, deps) {
return action$.pipe((0, operators_1.filter)((0, actions_2.isActionOf)(actions_3.transferSecretRequest)), (0, operators_1.filter)((action) => action.meta.direction === state_1.Direction.SENT), (0, operators_1.concatMap)((action) => secretReveal$(action, deps)));
}
exports.transferSecretRevealEpic = transferSecretRevealEpic;
/**
* Handles receiving a valid SecretReveal from recipient (neighbor/partner)
* This indicates that the partner knowws the secret, and we should Unlock to avoid going on-chain.
* The transferUnlock.request action is a request for the unlocking to be generated and sent.
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.config$ - Config observable
* @returns Observable of output actions for this epic
*/
function transferSecretRevealedEpic(action$, state$, { config$ }) {
return action$.pipe(
// we don't require Signed SecretReveal, nor even check sender for persisting the secret
(0, operators_1.filter)((0, utils_2.isMessageReceivedOfType)(types_1.SecretReveal)), (0, operators_1.withLatestFrom)(state$, config$), (0, operators_1.mergeMap)(function* ([action, state, { caps }]) {
const secrethash = (0, utils_4.getSecrethash)(action.payload.message.secret);
const results = Object.values(state_1.Direction)
.map((direction) => state.transfers[(0, utils_4.transferKey)({ secrethash, direction })])
.filter(types_2.isntNil);
const message = action.payload.message;
for (const sent of results.filter((doc) => doc.direction === state_1.Direction.SENT)) {
const meta = { secrethash, direction: state_1.Direction.SENT };
// if secrethash matches, we're good for persisting, don't care for sender/signature
yield (0, actions_3.transferSecret)({ secret: message.secret }, meta);
// but are stricter for unlocking to next hop only
if (action.meta.address === sent.partner &&
// don't unlock if channel closed: balanceProofs already registered on-chain
!sent.channelClosed
// accepts secretReveal/unlock request even if registered on-chain
) {
let viaPayload;
// unlock _through_ sender iff message is signed
if ('signature' in message)
viaPayload = { userId: action.payload.userId };
// request unlock to be composed, signed & sent to partner
yield actions_3.transferUnlock.request(viaPayload, meta);
}
}
// avoid unlocking received transfers if receiving is disabled
if (!(0, utils_3.getCap)(caps, constants_1.Capabilities.RECEIVE))
return;
// we're mediator or target, and received reveal from next hop or initiator, respectively
for (const _received of results.filter((doc) => doc.direction === state_1.Direction.RECEIVED)) {
// if secrethash matches, we're good for persisting, which also triggers Reveal back
yield (0, actions_3.transferSecret)({ secret: message.secret }, { secrethash, direction: state_1.Direction.RECEIVED });
}
}));
}
exports.transferSecretRevealedEpic = transferSecretRevealedEpic;
/**
* For a received transfer, when we know the secret, sign & send a SecretReveal to previous hop
*
* @param action$ - Observable of transferSecret|transferSecretReveal actions
* @param state$ - Observable of RaidenStates
* @param deps - RaidenEpicDeps
* @param deps.log - Logger instance
* @param deps.signer - Signer instance
* @param deps.latest$ - Latest observable
* @returns Observable of transferSecretReveal actions
*/
function transferRequestUnlockEpic(action$, {}, { log, signer, latest$ }) {
return action$.pipe((0, operators_1.filter)((0, actions_2.isActionOf)([actions_3.transferSecret, actions_3.transferSecretRegister.success])), (0, operators_1.filter)((action) => action.meta.direction === state_1.Direction.RECEIVED), (0, operators_1.filter)((action) => actions_3.transferSecret.is(action) || !!action.payload.confirmed), (0, operators_1.concatMap)((action) => latest$.pipe((0, rx_1.pluckDistinct)('state'), (0, operators_1.first)(), (0, operators_1.filter)(({ transfers }) => (0, utils_4.transferKey)(action.meta) in transfers), (0, operators_1.mergeMap)(({ transfers }) => {
const transferState = transfers[(0, utils_4.transferKey)(action.meta)];
const cached = transferState.secretReveal;
let signed$;
if (cached) {
signed$ = (0, rxjs_1.of)((0, types_2.untime)(cached));
}
else {
const message = {
type: types_1.MessageType.SECRET_REVEAL,
message_identifier: (0, utils_4.makeMessageId)(),
secret: action.payload.secret,
};
signed$ = (0, rxjs_1.from)((0, utils_2.signMessage)(signer, message, { log }));
}
const via = (0, utils_4.searchValidViaAddress)(transferState.transfer.metadata, transferState.partner);
return signed$.pipe((0, operators_1.map)((message) => (0, actions_3.transferSecretReveal)({ message, ...via }, action.meta)));
}), (0, operators_1.catchError)((err) => {
log.warn('Error trying to sign SecretReveal - ignoring', err, action.meta);
return rxjs_1.EMPTY;
}))));
}
exports.transferRequestUnlockEpic = transferRequestUnlockEpic;
/**
* Monitors SecretRegistry and emits when a relevant secret gets registered
*
* @param action$ - Observable of RaidenActions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.provider - Provider instance
* @param deps.secretRegistryContract - SecretRegistry contract instance
* @param deps.config$ - Config observable
* @param deps.init$ - Init$ tasks subject
* @param deps.getBlockTimestamp - block timestamp getter function
* @returns Observable of transferSecretRegister.success actions
*/
function monitorSecretRegistryEpic({}, state$, { provider, secretRegistryContract, config$, init$, getBlockTimestamp }) {
const initSub = new rxjs_1.AsyncSubject();
init$.next(initSub);
return (0, ethers_1.fromEthersEvent)(provider, secretRegistryContract.filters.SecretRevealed(null, null), {
confirmations: config$.pipe((0, operators_1.pluck)('confirmationBlocks')),
blockNumber$: state$.pipe((0, rx_1.pluckDistinct)('blockNumber')),
onPastCompleted: () => {
initSub.next(null);
initSub.complete();
},
}).pipe((0, rx_1.completeWith)(state$), (0, operators_1.map)((0, ethers_1.logToContractEvent)(secretRegistryContract)), (0, rx_1.withMergeFrom)(([, , event]) => getBlockTimestamp(event.blockNumber)), (0, operators_1.withLatestFrom)(state$, config$), (0, operators_1.mergeMap)(function* ([[[secrethash, secret, event], txTimestamp], { transfers, blockNumber }, { confirmationBlocks },]) {
// find sent|received transfers matching secrethash and secret registered before expiration
for (const direction of Object.values(state_1.Direction)) {
const key = (0, utils_4.transferKey)({ secrethash: secrethash, direction });
const transferState = transfers[key];
if (!transferState || transferState.expiration <= txTimestamp)
continue;
yield actions_3.transferSecretRegister.success({
secret: secret,
txTimestamp,
txHash: event.transactionHash,
txBlock: event.blockNumber,
confirmed: event.blockNumber + confirmationBlocks <= blockNumber ? !event.removed : undefined,
}, { secrethash: secrethash, direction });
}
}));
}
exports.monitorSecretRegistryEpic = monitorSecretRegistryEpic;
/**
* A simple epic to emit transfer.success when secret register is confirmed
*
* @param action$ - Observable of transferSecretRegister.success actions
* @returns Observable of transfer.success actions
*/
function transferSuccessOnSecretRegisteredEpic(action$) {
return action$.pipe((0, operators_1.filter)(actions_3.transferSecretRegister.success.is), (0, operators_1.filter)((action) => !!action.payload.confirmed), (0, operators_1.map)((action) => actions_3.transfer.success({}, action.meta)));
}
exports.transferSuccessOnSecretRegisteredEpic = transferSuccessOnSecretRegisteredEpic;
/**
* Process newBlocks and pending received transfers. If we know the secret, and transfer doesn't
* get unlocked before revealTimeout seconds are left to lock expiration, request to register secret
* TODO: check economic viability (and define what that means) of registering lock on-chain
*
* @param action$ - Observable of newBlock actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @param deps.config$ - Config observable
* @returns Observable of transferSecretRegister.request actions
*/
function transferAutoRegisterEpic(action$, state$, { config$ }) {
const transferIsGone$ = ({ key }) => state$.pipe((0, operators_1.filter)(({ transfers }) => !(key in transfers)));
return state$.pipe((0, operators_1.pluck)('transfers'), (0, operators_1.mergeMap)((transfers) => Object.values(transfers)), (0, operators_1.filter)(({ direction }) => direction === state_1.Direction.RECEIVED), (0, operators_1.groupBy)(({ _id }) => _id, { duration: transferIsGone$ }), (0, operators_1.mergeMap)((grouped$) => grouped$.pipe((0, operators_1.filter)(({ secret }) => !!secret), (0, operators_1.withLatestFrom)(config$), (0, operators_1.delayWhen)(([{ expiration }, { revealTimeout }]) =>
// danger zone!
(0, rxjs_1.timer)(new Date((expiration - revealTimeout) * 1e3))), (0, operators_1.exhaustMap)(([{ secret, transfer: locked }]) => {
const meta = { secrethash: locked.lock.secrethash, direction: state_1.Direction.RECEIVED };
return (0, utils_5.dispatchAndWait$)(action$, actions_3.transferSecretRegister.request({ secret: secret }, meta), (0, actions_2.isConfirmationResponseOf)(actions_3.transferSecretRegister, meta));
}), (0, operators_1.takeUntil)(grouped$.pipe((0, operators_1.filter)((t) => !!(t.unlock ||
t.expired ||
t.secretRegistered ||
t.channelClosed ||
// gives up immediatelly if already over expiration
t.expiration <= Date.now() / 1e3)), (0, operators_1.mergeWith)(transferIsGone$(grouped$)))), (0, rx_1.completeWith)(state$))));
}
exports.transferAutoRegisterEpic = transferAutoRegisterEpic;
/**
* Registers secret on-chain. Success is detected by monitorSecretRegistryEpic
*
* @param action$ - Observable of transferSecretRegister.request actions
* @param state$ - Observable of RaidenStates
* @param deps - Epics dependencies
* @returns Observable of transferSecretRegister.failure actions
*/
function transferSecretRegisterEpic(action$, {}, deps) {
const { log, secretRegistryContract, config$ } = deps;
return action$.pipe((0, operators_1.filter)(actions_3.transferSecretRegister.request.is), (0, operators_1.mergeMap)((action) => (0, utils_1.transact)(secretRegistryContract, 'registerSecret', [action.payload.secret], deps, {
error: error_1.ErrorCodes.XFER_REGISTERSECRET_TX_FAILED,
}).pipe((0, rx_1.retryWhile)((0, config_1.intervalFromConfig)(config$), { onErrors: error_1.commonTxErrors, log: log.debug }),
// transferSecretRegister.success handled by monitorSecretRegistryEpic
(0, operators_1.ignoreElements)(), (0, operators_1.catchError)((err) => (0, rxjs_1.of)(actions_3.transferSecretRegister.failure(err, action.meta))))));
}
exports.transferSecretRegisterEpic = transferSecretRegisterEpic;
//# sourceMappingURL=secret.js.map