UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

206 lines 8.6 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import * as t from 'io-ts'; import isMatchWith from 'lodash/isMatchWith'; import { firstValueFrom } from 'rxjs'; import { filter, map, take } from 'rxjs/operators'; import { assert } from '../utils'; import { ErrorCodes, RaidenError } from '../utils/error'; import { BigNumberC } from './types'; /** * @param args - Args params * @returns action creator */ export function createAction(...args) { const [type, payloadCodec, metaCodec, error] = args; const codec = t.readonly(t.type({ type: t.literal(type), ...(payloadCodec ? { payload: payloadCodec } : null), ...(metaCodec ? { meta: metaCodec } : null), ...(error ? { error: t.literal(true) } : null), })); const is = process.env['NODE_ENV'] === 'development' ? (action) => codec.is(action) : (action) => action?.['type'] === type; return Object.assign(function actionFactory(payload, meta) { return { type, ...(payloadCodec ? { payload } : {}), ...(metaCodec ? { meta } : {}), ...(error ? { error } : {}), }; }, { type, is, codec, error }); } /** * Curried typeguard function (arity=2) which validates 2nd param is of type of some ActionCreators * * @param ac - Single or array of ActionCreators * @param args - if an object is passed, verify it, else returns a function which does * @returns boolean indicating object is of type of action, if passing 2nd argument, * or typeguard function */ export function isActionOf(ac, ...args) { function _isActionOf(action) { if (typeof this === 'function') return this.is(action); if (Array.isArray(this)) return this.some((a) => _isActionOf.call(a, action)); if (typeof this === 'object') return _isActionOf.call(Object.values(this), action); return false; } if (args.length > 0) return _isActionOf.call(ac, args[0]); return _isActionOf.bind(ac); } /** * Create a set of async actions * * Here, meta is first class citizen, as it's required and what links a request with its responses * (success or failure). * * @param args - Arguments tuple; [meta, type] are required, while [request, success an failure] * are codecs to be used as payloads for the respective ActionCreators * @returns Async actions */ export function createAsyncAction(...args) { return { request: createAction(`${args[1]}/request`, args[2], args[0]), success: createAction(`${args[1]}/success`, args[3], args[0]), failure: createAction(`${args[1]}/failure`, (args[4] ?? t.unknown), args[0], true), }; } /** * Match a passed meta with an action if returns true if metas are from corresponding actions * * curried (arity=2) for action passed as 2nd param. * * @param meta - meta base for comparison * @param args - curried args array * @returns true if metas are compatible, false otherwise */ function matchMeta(meta, ...args) { const _match = (action) => // like isEqual, but for BigNumbers, use .eq isMatchWith(action.meta, meta, (objVal, othVal) => // any is to avoid lodash's issue with undefined-returning isMatchWithCustomizer cb type BigNumberC.is(objVal) && BigNumberC.is(othVal) ? objVal.eq(othVal) : undefined); if (args.length) return _match(args[0]); return _match; } /** * Given an AsyncActionCreator and a respective 'meta' object, returns a type guard function for * responses actions (success|failure) matching given 'meta' * * This function receives 2-3 params. If it receives 2, it returns the type guard function, to be * used for filtering. Otherwise, it performs the check on the 3rd param. * * @param asyncAction - AsyncActionCreator object * @param meta - meta object to filter matching actions * @param args - curried last param * @returns type guard function to filter deep-equal meta success|failure actions */ export function isResponseOf(asyncAction, meta, ...args) { const _isResponseOf = (action) => isActionOf([asyncAction.success, asyncAction.failure], action) && matchMeta(meta, action); if (args.length) return _isResponseOf(args[0]); return _isResponseOf; } /** * Like isResponseOf, but ignores non-confirmed (or removed by a reorg) success action * * Confirmable success actions are emitted twice: first with payload.confirmed=undefined, then with * either confirmed=true, if tx still present after confirmation blocks, or confirmed=false, if tx * was removed from blockchain by a reorg. * This curied helper filter function ensures only one of the later causes a positive filter. * * @param asyncAction - AsyncActionCreator object * @param meta - meta object to filter matching actions * @param args - curried last param * @returns type guard function to filter deep-equal meta success|failure actions */ export function isConfirmationResponseOf(asyncAction, meta, ...args) { /** * @param action - action to check * @returns boolean indicating whether object is confirmation */ function _isConfirmation(action) { return typeof action?.['payload']?.['confirmed'] === 'boolean'; } const _isResponseOf = (action) => isResponseOf(asyncAction, meta, action) && (asyncAction.failure.is(action) || _isConfirmation(action)); if (args.length) return _isResponseOf(args[0]); return _isResponseOf; } /** * Watch a stream of actions and resolves on meta-matching success or rejects on failure * * @param asyncAction - async actions object to wait for * @param meta - meta object of a request to wait for the respective response * @param action$ - actions stream to watch for responses * @param confirmed - undefined for any response action, false to filter confirmable actions, * true for confirmed ones * @returns Promise which rejects with payload in case of failure, or resolves payload otherwise */ export async function asyncActionToPromise(asyncAction, meta, action$, confirmed) { return firstValueFrom(action$.pipe(filter(confirmed ? isConfirmationResponseOf(asyncAction, meta) : isResponseOf(asyncAction, meta)), filter((action) => confirmed === undefined || !asyncAction.success.is(action) || 'confirmed' in action.payload), take(1), map((action) => { if (asyncAction.failure.is(action)) throw action.payload; else if (action.payload?.confirmed === false) throw new RaidenError(ErrorCodes.RDN_TRANSACTION_REORG, { transactionHash: action.payload.txHash, }); return action.payload; })), { defaultValue: undefined }); } // createReducer /** * Create a reducer which can be extended with additional actions handlers * * Usage: * const reducer = createReducer(State) * .handle(action, (s, a): State => ...) * .handle(...) * .handle(...); * * @param initialState - state for initialization (if no state is passed on reducer call) * @returns A reducer function, extended with a handle method to extend it */ export function createReducer(initialState) { /** * Make a reducer function for given handlers * * @param handlers - handlers to put into the reducer * @returns reducer function for given handlers */ function makeReducer(handlers) { const reducer = (state = initialState, action) => { const handler = handlers[action.type]; if (handler && handler[0].is(action)) return handler[1](state, action); // calls registered handler return state; // fallback returns unchanged state }; /** * Circular dependency on generic params forbids an already handled action from being accepted * * @param ac - Single or array of ActionCreators * @param handler - handler to use * @returns reducer with the action created incorporated */ function handle(ac, handler) { const arr = Array.isArray(ac) ? ac : [ac]; assert(!arr.some((a) => a.type in handlers), 'Already handled'); return makeReducer(Object.assign({}, handlers, ...arr.map((ac) => ({ [ac.type]: [ac, handler] })))); } // grow reducer function with our `handle` extender return Object.assign(reducer, { handle }); } // initially makes a reducer which doesn't handle anything (just returns unchanged state) return makeReducer({}); } //# sourceMappingURL=actions.js.map