Skip to content

Commit

Permalink
Moved/altered error codec & added PFS errors
Browse files Browse the repository at this point in the history
  • Loading branch information
nephix committed Feb 13, 2020
1 parent b7a94fc commit 663e9bb
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 66 deletions.
2 changes: 1 addition & 1 deletion raiden-ts/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as t from 'io-ts';
import { ShutdownReason } from './constants';
import { PartialRaidenConfig } from './config';
import { ActionType, createAction, Action } from './utils/actions';
import { ErrorCodec } from './utils/types';
import { ErrorCodec } from './utils/error';

import * as ChannelsActions from './channels/actions';
import * as TransportActions from './transport/actions';
Expand Down
41 changes: 28 additions & 13 deletions raiden-ts/src/path/epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ import { Address, decode, Int, Signature, Signed, UInt } from '../utils/types';
import { isActionOf } from '../utils/actions';
import { encode, losslessParse, losslessStringify } from '../utils/data';
import { getEventsStream } from '../utils/ethers';
import { PfsError } from '../utils/error';
import { iouClear, pathFind, iouPersist, pfsListUpdated } from './actions';
import { channelCanRoute, pfsInfo, pfsListInfo } from './utils';
import { IOU, LastIOUResults, PathResults, Paths, PFS } from './types';
import { PfsErrorCodes } from './errors';

const oneToNAddress = memoize(
async (userDepositContract: UserDeposit) =>
Expand Down Expand Up @@ -148,16 +150,21 @@ const prepareNextIOU$ = (
}
const text = await response.text();
if (!response.ok)
throw new Error(
`PFS: last IOU request: code=${response.status} => body="${text}"`,
);
throw new PfsError(PfsErrorCodes.PFS_LAST_IOU_REQUEST_FAILED, [
{ key: 'responseStatus', value: response.status },
{ key: 'responseText', value: text },
]);

const { last_iou: lastIou } = decode(LastIOUResults, losslessParse(text));
const signer = verifyMessage(packIOU(lastIou), lastIou.signature);
if (signer !== deps.address)
throw new Error(
`PFS: last iou signature mismatch: signer=${signer} instead of us ${deps.address}`,
);
throw new PfsError(PfsErrorCodes.PFS_IOU_SIGNATURE_MISMATCH, [
{
key: 'signer',
value: signer,
},
{ key: 'address', value: deps.address },
]);
return lastIou;
}),
)
Expand Down Expand Up @@ -201,9 +208,16 @@ export const pathFindServiceEpic = (
mergeMap(([state, presences, { pfs: configPfs, httpTimeout, pfsSafetyMargin }]) => {
const { tokenNetwork, target } = action.meta;
if (!(tokenNetwork in state.channels))
throw new Error(`PFS: unknown tokenNetwork ${tokenNetwork}`);
throw new PfsError(PfsErrorCodes.PFS_UNKNOWN_TOKEN_NETWORK, [
{ key: 'tokenNetwork', value: tokenNetwork },
]);
if (!(target in presences) || !presences[target].payload.available)
throw new Error(`PFS: target ${target} not online`);
throw new PfsError(PfsErrorCodes.PFS_TARGET_OFFLINE, [
{
key: 'target',
value: target,
},
]);

// if pathFind received a set of paths, pass it through to validation/cleanup
if (action.payload.paths) return of({ paths: action.payload.paths, iou: undefined });
Expand All @@ -220,7 +234,7 @@ export const pathFindServiceEpic = (
(!action.payload.pfs && configPfs === null) // disabled in config and not provided
) {
// pfs not specified in action and disabled (null) in config
throw new Error(`PFS disabled and no direct route available`);
throw new PfsError(PfsErrorCodes.PFS_DISABLED);
} else {
// else, request a route from PFS.
// pfs$ - Observable which emits one PFS info and then completes
Expand Down Expand Up @@ -328,9 +342,10 @@ export const pathFindServiceEpic = (
}
// if error, don't proceed
if (!data.paths) {
throw new Error(
`PFS: paths request: code=${data.error.error_code} => errors="${data.error.errors}"`,
);
throw new PfsError(PfsErrorCodes.PFS_ERROR_RESPONSE, [
{ key: 'errorCode', value: data.error.error_code },
{ key: 'errors', value: data.error.errors },
]);
}
const filteredPaths: Paths = [],
invalidatedRecipients = new Set<Address>();
Expand Down Expand Up @@ -367,7 +382,7 @@ export const pathFindServiceEpic = (
}
filteredPaths.push({ path, fee });
}
if (!filteredPaths.length) throw new Error(`PFS: no valid routes found`);
if (!filteredPaths.length) throw new PfsError(PfsErrorCodes.PFS_NO_ROUTES_FOUND);
yield pathFind.success({ paths: filteredPaths }, action.meta);
})(),
),
Expand Down
12 changes: 12 additions & 0 deletions raiden-ts/src/path/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export enum PfsErrorCodes {
PFS_EMPTY_URL = 'A registered Pathfinding Service returned an empty service URL.',
PFS_INVALID_URL = 'A registered Pathfinding Service returned an invalid service URL.',
PFS_INVALID_INFO = 'Could not any valid Pathfinding services. Client and PFS versions are possibly out-of-sync.',
PFS_NO_ROUTES_FOUND = 'No valid routes found.',
PFS_ERROR_RESPONSE = 'Pathfinding Service request returned an error',
PFS_DISABLED = 'Pathfinding Service is disabled and no direct route is available.',
PFS_UNKNOWN_TOKEN_NETWORK = 'Unknown token network.',
PFS_TARGET_OFFLINE = 'The requested target is offline.',
PFS_LAST_IOU_REQUEST_FAILED = 'The request for the last IOU has failed.',
PFS_IOU_SIGNATURE_MISMATCH = 'The signature of the last IOU did not match.',
}
12 changes: 6 additions & 6 deletions raiden-ts/src/path/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { Presences } from '../transport/types';
import { ChannelState } from '../channels/state';
import { channelAmounts } from '../channels/utils';
import { ServiceRegistry } from '../contracts/ServiceRegistry';
import { PfsError } from '../utils/error';
import { PFS } from './types';
import { PfsErrorCodes } from './errors';

/**
* Either returns true if given channel can route a payment, or a reason as string if not
Expand Down Expand Up @@ -86,8 +88,9 @@ export function pfsInfo(
return url$.pipe(
withLatestFrom(config$),
mergeMap(([url, { httpTimeout }]) => {
if (!url) throw new Error(`Empty URL: ${url}`);
else if (!urlRegex.test(url)) throw new Error(`Invalid URL: ${url}`);
if (!url) throw new PfsError(PfsErrorCodes.PFS_EMPTY_URL);
else if (!urlRegex.test(url))
throw new PfsError(PfsErrorCodes.PFS_EMPTY_URL, [{ key: 'url', value: url }]);
// default to https for domain-only urls
else if (!url.startsWith('https://')) url = `https://${url}`;

Expand Down Expand Up @@ -141,10 +144,7 @@ export function pfsListInfo(
),
toArray(),
map(list => {
if (!list.length)
throw new Error(
'Could not validate any PFS info. Possibly out-of-sync with PFSs version.',
);
if (!list.length) throw new PfsError(PfsErrorCodes.PFS_INVALID_INFO);
return list.sort((a, b) => {
const dif = a.price.sub(b.price);
// first, sort by price
Expand Down
3 changes: 2 additions & 1 deletion raiden-ts/src/utils/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { Observable } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { Reducer } from 'redux';

import { ErrorCodec, BigNumberC, assert } from './types';
import { ErrorCodec } from './error';
import { BigNumberC, assert } from './types';

/**
* The type of a generic action
Expand Down
90 changes: 76 additions & 14 deletions raiden-ts/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,88 @@
export type ErrorDetail = { [key: string]: string };
import * as t from 'io-ts';
import { map } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';

export default abstract class RaidenError extends Error {
import { PfsErrorCodes } from '../path/errors';

export const ErrorDetails = t.array(
t.type({
key: t.string,
value: t.union([t.string, t.number]),
}),
);
export interface ErrorDetails extends t.TypeOf<typeof ErrorDetails> {}

export default class RaidenError extends Error {
code: string;
details?: ErrorDetail[];
details?: ErrorDetails;

constructor(message: string, detail?: ErrorDetail[] | ErrorDetail) {
constructor(message: string, details?: ErrorDetails) {
super(message || 'General Error');
this.name = 'RaidenError';
this.code = this.getCode(message);
this.details = details;
}

if (detail) {
this.details = Array.isArray(detail) ? detail : [detail];
}
getCode(message: string): string {
return message ?? 'RAIDEN_ERROR';
}
}

addDetail(detail: ErrorDetail) {
if (this.details) {
this.details.push(detail);
} else {
this.details = [detail];
}
export class PfsError extends RaidenError {
constructor(message: PfsErrorCodes, details?: ErrorDetails) {
super(message, details);
this.name = 'PfsError';
}

abstract getCode(message: string): string;
getCode(message: string): string {
return (
Object.keys(PfsErrorCodes).find(code => Object(PfsErrorCodes)[code] === message) ??
'PFS_GENERAL_ERROR'
);
}
}

const serializedErr = t.intersection([
t.type({ name: t.string, message: t.string, code: t.string }),
t.partial({ stack: t.string, details: ErrorDetails }),
]);

/**
* Simple Error codec
*
* This codec doesn't decode to an instance of the exact same error class object, but instead to
* a generic Error, but assigning 'name', 'stack' & 'message' properties, more as an informative
* object.
*/
export const ErrorCodec = new t.Type<
RaidenError,
{ name: string; message: string; code: string; stack?: string; details?: ErrorDetails }
>(
'RaidenError',
(u: unknown): u is RaidenError => u instanceof RaidenError,
u => {
if (u instanceof RaidenError) return t.success(u);
return pipe(
serializedErr.decode(u),
map(({ name, message, code, stack, details }) => {
switch (name) {
case 'PfsError':
return Object.assign(new PfsError(message as PfsErrorCodes, details), {
name,
stack,
code,
});
}

return Object.assign(new RaidenError(message), { name, stack });
}),
);
},
({ name, message, stack, details, code }) => ({
name,
message,
stack,
code,
details: details ?? undefined,
}),
);
28 changes: 1 addition & 27 deletions raiden-ts/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import * as t from 'io-ts';
import { BigNumber, bigNumberify, getAddress, isHexString, hexDataLength } from 'ethers/utils';
import { Two, Zero } from 'ethers/constants';
import { memoize } from 'lodash';
import { Either, Right, map } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { Either, Right } from 'fp-ts/lib/Either';
import { ThrowReporter } from 'io-ts/lib/ThrowReporter';

/* A Subset of DOM's Storage/localStorage interface which supports async/await */
Expand Down Expand Up @@ -59,31 +58,6 @@ export function isntNil<T>(value: T): value is NonNullable<T> {
return value != null;
}

const serializedErr = t.intersection([
t.type({ name: t.string, message: t.string }),
t.partial({ stack: t.string }),
]);

/**
* Simple Error codec
*
* This codec doesn't decode to an instance of the exact same error class object, but instead to
* a generic Error, but assigning 'name', 'stack' & 'message' properties, more as an informative
* object.
*/
export const ErrorCodec = new t.Type<Error, { name: string; message: string; stack?: string }>(
'Error',
(u: unknown): u is Error => u instanceof Error,
u => {
if (u instanceof Error) return t.success(u);
return pipe(
serializedErr.decode(u),
map(({ name, message, stack }) => Object.assign(new Error(message), { name, stack })),
);
},
e => ({ name: e.name, message: e.message, stack: e.stack }),
);

/**
* Codec of ethers.utils.BigNumber objects
*
Expand Down
8 changes: 4 additions & 4 deletions raiden-ts/tests/unit/error.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { isError } from 'util';

import RaidenError, { ErrorDetail } from '../../src/utils/error';
import RaidenError, { ErrorDetails } from '../../src/utils/error';

enum MyCustomErrorCodes {
TEST = 'Developer-friendly description',
}

class MyCustomError extends RaidenError {
constructor(message: MyCustomErrorCodes, detail?: ErrorDetail) {
constructor(message: MyCustomErrorCodes, detail?: ErrorDetails) {
super(message, detail);
this.name = 'MyCustomError';
}
Expand Down Expand Up @@ -70,9 +70,9 @@ describe('Test custom error', () => {

test('Details can be added and are shown in stack trace', () => {
try {
throw new MyCustomError(MyCustomErrorCodes.TEST, { foo: 'bar' });
throw new MyCustomError(MyCustomErrorCodes.TEST, [{ value: 'bar', key: 'foo' }]);
} catch (err) {
expect(err.details).toEqual([{ foo: 'bar' }]);
expect(err.details).toEqual([{ value: 'bar', key: 'foo' }]);
}
});
});

0 comments on commit 663e9bb

Please sign in to comment.