Skip to content

Commit

Permalink
Implement SDK error handling (#1014)
Browse files Browse the repository at this point in the history
* Initial proposal

* Moved/altered error codec & added PFS errors

* Removed PfsError and simplified error codec

* Fixed pfs tests

* Fixed custom error tests

* Added raiden errors to channel and transfer epics; Simplified error codec

* Converted raiden, transport and channel errors

* Update raiden-ts/src/utils/error.ts

Co-Authored-By: André Vitor de Lima Matos <[email protected]>

* Simplified error details in path epic

* Replaced remaining errors with raiden errors

* Fixed unit tests

* Fixed e2e tests

* Moved RaidenError tests to utils

* Attempt e2e test fix

* Remove ts-custom-error & fix ErrorCodec & tests

* Added waitTransfer error

* Added changelog entry

Co-authored-by: André Vitor de Lima Matos <[email protected]>
  • Loading branch information
nephix and andrevmatos authored Feb 20, 2020
1 parent e1882aa commit d5d245b
Show file tree
Hide file tree
Showing 21 changed files with 384 additions and 168 deletions.
3 changes: 3 additions & 0 deletions raiden-ts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
### Added
- [#614] Implement state upgrades and migration
- [#613] Implement waiting for confirmation blocks on on-chain transactions (configurable)
- [#1000] Implemented SDK error handling

### Changed
- [#926] Introduce loglevel logging framework (config.logger now specifies logging level)
- [#1042] Support decoding addresses on messages on lowercased format

[#1000]: https://github.com/raiden-network/light-client/issues/1000

## [0.3.0] - 2020-02-07
### Added
- [#172] Add derived subkey support
Expand Down
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
59 changes: 37 additions & 22 deletions raiden-ts/src/channels/epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { isActionOf } from '../utils/actions';
import { pluckDistinct } from '../utils/rx';
import { fromEthersEvent, getEventsStream, getNetwork } from '../utils/ethers';
import { encode } from '../utils/data';
import RaidenError, { ErrorCodes } from '../utils/error';
import {
newBlock,
tokenMonitored,
Expand Down Expand Up @@ -496,7 +497,10 @@ export const channelOpenEpic = (
// proceed only if channel is in 'opening' state, set by this action
if (channelState !== ChannelState.opening)
return of(
channelOpen.failure(new Error(`Invalid channel state: ${channelState}`), action.meta),
channelOpen.failure(
new RaidenError(ErrorCodes.CNL_INVALID_STATE, { state: channelState.toString() }),
action.meta,
),
);

// send openChannel transaction !!!
Expand All @@ -509,7 +513,10 @@ export const channelOpenEpic = (
).pipe(
mergeMap(async tx => ({ receipt: await tx.wait(), tx })),
map(({ receipt, tx }) => {
if (!receipt.status) throw new Error(`openChannel transaction "${tx.hash}" failed`);
if (!receipt.status)
throw new RaidenError(ErrorCodes.CNL_OPENCHANNEL_FAILED, {
transactionHash: tx.hash!,
});
return tx.hash;
}),
// if succeeded, return a empty/completed observable
Expand Down Expand Up @@ -588,7 +595,9 @@ export const channelDepositEpic = (
| Address
| undefined;
if (!token) {
const error = new Error(`token for tokenNetwork "${action.meta.tokenNetwork}" not found`);
const error = new RaidenError(ErrorCodes.CNL_TOKEN_NOT_FOUND, {
tokenNetwork: action.meta.tokenNetwork,
});
return of(channelDeposit.failure(error, action.meta));
}
const { signer: onchainSigner } = chooseOnchainAccount(
Expand All @@ -605,9 +614,7 @@ export const channelDepositEpic = (
action.meta.partner,
]);
if (!channel || channel.state !== ChannelState.open || channel.id === undefined) {
const error = new Error(
`channel for "${action.meta.tokenNetwork}" and "${action.meta.partner}" not found or not in 'open' state`,
);
const error = new RaidenError(ErrorCodes.CNL_NO_OPEN_CHANNEL_FOUND, action.meta);
return of(channelDeposit.failure(error, action.meta));
}
const channelId = channel.id;
Expand All @@ -621,7 +628,10 @@ export const channelDepositEpic = (
mergeMap(async tx => ({ receipt: await tx.wait(), tx })),
map(({ receipt, tx }) => {
if (!receipt.status)
throw new Error(`token "${token}" approve transaction "${tx.hash}" failed`);
throw new RaidenError(ErrorCodes.CNL_APPROVE_TRANSACTION_FAILED, {
token,
transactionHash: tx.hash!,
});
return tx.hash;
}),
tap(txHash => log.debug(`approve tx "${txHash}" successfuly mined!`)),
Expand All @@ -645,9 +655,10 @@ export const channelDepositEpic = (
mergeMap(async tx => ({ receipt: await tx.wait(), tx })),
map(({ receipt, tx }) => {
if (!receipt.status)
throw new Error(
`tokenNetwork "${action.meta.tokenNetwork}" setTotalDeposit transaction "${tx.hash}" failed`,
);
throw new RaidenError(ErrorCodes.CNL_SETTOTALDEPOSIT_FAILED, {
tokenNetwork: action.meta.tokenNetwork,
transactionHash: tx.hash!,
});
return tx.hash;
}),
tap(txHash => log.debug(`setTotalDeposit tx "${txHash}" successfuly mined!`)),
Expand Down Expand Up @@ -699,9 +710,10 @@ export const channelCloseEpic = (
!(channel.state === ChannelState.open || channel.state === ChannelState.closing) ||
!channel.id
) {
const error = new Error(
`channel for "${action.meta.tokenNetwork}" and "${action.meta.partner}" not found or not in 'open' or 'closing' state`,
);
const error = new RaidenError(ErrorCodes.CNL_NO_OPEN_OR_CLOSING_CHANNEL_FOUND, {
tokenNetwork: action.meta.tokenNetwork,
partner: action.meta.partner,
});
return of(channelClose.failure(error, action.meta));
}
const channelId = channel.id;
Expand Down Expand Up @@ -753,9 +765,10 @@ export const channelCloseEpic = (
mergeMap(async tx => ({ receipt: await tx.wait(), tx })),
map(({ receipt, tx }) => {
if (!receipt.status)
throw new Error(
`tokenNetwork "${action.meta.tokenNetwork}" closeChannel transaction "${tx.hash}" failed`,
);
throw new RaidenError(ErrorCodes.CNL_CLOSECHANNEL_FAILED, {
tokenNetwork: action.meta.tokenNetwork,
transactionHash: tx.hash!,
});
log.debug(`closeChannel tx "${tx.hash}" successfuly mined!`);
return tx.hash;
}),
Expand Down Expand Up @@ -807,9 +820,10 @@ export const channelSettleEpic = (
!(channel.state === ChannelState.settleable || channel.state === ChannelState.settling) ||
!channel.id
) {
const error = new Error(
`channel for "${action.meta.tokenNetwork}" and "${action.meta.partner}" not found or not in 'settleable' or 'settling' state`,
);
const error = new RaidenError(ErrorCodes.CNL_NO_SETTLEABLE_OR_SETTLING_CHANNEL_FOUND, {
tokenNetwork: action.meta.tokenNetwork,
partner: action.meta.partner,
});
return of(channelSettle.failure(error, action.meta));
}
const channelId = channel.id;
Expand Down Expand Up @@ -849,9 +863,10 @@ export const channelSettleEpic = (
mergeMap(async tx => ({ receipt: await tx.wait(), tx })),
map(({ receipt, tx }) => {
if (!receipt.status)
throw new Error(
`tokenNetwork "${action.meta.tokenNetwork}" settleChannel transaction "${tx.hash}" failed`,
);
throw new RaidenError(ErrorCodes.CNL_SETTLECHANNEL_FAILED, {
tokenNetwork: action.meta.tokenNetwork,
transactionHash: tx.hash!,
});
log.debug(`settleChannel tx "${tx.hash}" successfuly mined!`);
return tx.hash;
}),
Expand Down
17 changes: 11 additions & 6 deletions raiden-ts/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import goerliDeploy from './deployment/deployment_goerli.json';
import ropstenServicesDeploy from './deployment/deployment_services_ropsten.json';
import rinkebyServicesDeploy from './deployment/deployment_services_rinkeby.json';
import goerliServicesDeploy from './deployment/deployment_services_goerli.json';
import RaidenError, { ErrorCodes } from './utils/error';

/**
* Returns contract information depending on the passed [[Network]]. Currently, only
Expand Down Expand Up @@ -48,9 +49,7 @@ export const getContracts = (network: Network): ContractsInfo => {
...goerliServicesDeploy.contracts,
} as unknown) as ContractsInfo;
default:
throw new Error(
`No deploy info provided nor recognized network: ${JSON.stringify(network)}`,
);
throw new RaidenError(ErrorCodes.RDN_UNRECOGNIZED_NETWORK, { network: network.name });
}
};

Expand Down Expand Up @@ -102,7 +101,10 @@ export const getSigner = async (
} else if (account instanceof Wallet) {
signer = account.connect(provider);
} else {
throw new Error(`Signer ${account} not connected to ${provider}`);
throw new RaidenError(ErrorCodes.RDN_SIGNER_NOT_CONNECTED, {
account: account.toString(),
provider: provider.toString(),
});
}
address = (await signer.getAddress()) as Address;
} else if (typeof account === 'number') {
Expand All @@ -113,7 +115,10 @@ export const getSigner = async (
// address
const accounts = await provider.listAccounts();
if (!accounts.includes(account)) {
throw new Error(`Account "${account}" not found in provider, got=${accounts}`);
throw new RaidenError(ErrorCodes.RDN_ACCOUNT_NOT_FOUND, {
account,
accounts: JSON.stringify(accounts),
});
}
signer = provider.getSigner(account);
address = account;
Expand All @@ -122,7 +127,7 @@ export const getSigner = async (
signer = new Wallet(account, provider);
address = signer.address as Address;
} else {
throw new Error('String account must be either a 0x-encoded address or private key');
throw new RaidenError(ErrorCodes.RDN_STRING_ACCOUNT_INVALID);
}

if (subkey) {
Expand Down
30 changes: 17 additions & 13 deletions raiden-ts/src/path/epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ 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 RaidenError, { ErrorCodes } from '../utils/error';
import { iouClear, pathFind, iouPersist, pfsListUpdated } from './actions';
import { channelCanRoute, pfsInfo, pfsListInfo } from './utils';
import { IOU, LastIOUResults, PathResults, Paths, PFS } from './types';
Expand Down Expand Up @@ -148,16 +149,18 @@ const prepareNextIOU$ = (
}
const text = await response.text();
if (!response.ok)
throw new Error(
`PFS: last IOU request: code=${response.status} => body="${text}"`,
);
throw new RaidenError(ErrorCodes.PFS_LAST_IOU_REQUEST_FAILED, {
responseStatus: response.status,
responseText: 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 RaidenError(ErrorCodes.PFS_IOU_SIGNATURE_MISMATCH, {
signer,
address: deps.address,
});
return lastIou;
}),
)
Expand Down Expand Up @@ -202,9 +205,9 @@ 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 RaidenError(ErrorCodes.PFS_UNKNOWN_TOKEN_NETWORK, { tokenNetwork });
if (!(target in presences) || !presences[target].payload.available)
throw new Error(`PFS: target ${target} not online`);
throw new RaidenError(ErrorCodes.PFS_TARGET_OFFLINE, { 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 @@ -221,7 +224,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 RaidenError(ErrorCodes.PFS_DISABLED);
} else {
// else, request a route from PFS.
// pfs$ - Observable which emits one PFS info and then completes
Expand Down Expand Up @@ -329,9 +332,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 RaidenError(ErrorCodes.PFS_ERROR_RESPONSE, {
errorCode: data.error.error_code,
errors: data.error.errors,
});
}
const filteredPaths: Paths = [],
invalidatedRecipients = new Set<Address>();
Expand Down Expand Up @@ -368,7 +372,7 @@ export const pathFindServiceEpic = (
}
filteredPaths.push({ path, fee });
}
if (!filteredPaths.length) throw new Error(`PFS: no valid routes found`);
if (!filteredPaths.length) throw new RaidenError(ErrorCodes.PFS_NO_ROUTES_FOUND);
yield pathFind.success({ paths: filteredPaths }, action.meta);
})(),
),
Expand Down
10 changes: 4 additions & 6 deletions raiden-ts/src/path/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Presences } from '../transport/types';
import { ChannelState } from '../channels/state';
import { channelAmounts } from '../channels/utils';
import { ServiceRegistry } from '../contracts/ServiceRegistry';
import RaidenError, { ErrorCodes } from '../utils/error';
import { PFS } from './types';

/**
Expand Down Expand Up @@ -86,8 +87,8 @@ 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 RaidenError(ErrorCodes.PFS_EMPTY_URL);
else if (!urlRegex.test(url)) throw new RaidenError(ErrorCodes.PFS_INVALID_URL, { url });
// default to https for domain-only urls
else if (!url.startsWith('https://')) url = `https://${url}`;

Expand Down Expand Up @@ -142,10 +143,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 RaidenError(ErrorCodes.PFS_INVALID_INFO);
return list.sort((a, b) => {
const dif = a.price.sub(b.price);
// first, sort by price
Expand Down
14 changes: 8 additions & 6 deletions raiden-ts/src/raiden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
chooseOnchainAccount,
getContractWithSigner,
} from './helpers';
import RaidenError, { ErrorCodes } from './utils/error';

export class Raiden {
private readonly store: Store<RaidenState, RaidenAction>;
Expand Down Expand Up @@ -794,7 +795,7 @@ export class Raiden {
// already completed/past transfer
if (sent.completed) {
if (sent.success) return this.state.sent[secrethash].secretRequest?.[1]?.amount;
else throw new Error(sent.status);
else throw new RaidenError(ErrorCodes.XFER_ALREADY_COMPLETED, { status: sent.status });
}

// throws/rejects if a failure occurs
Expand Down Expand Up @@ -925,7 +926,8 @@ export class Raiden {
const customTokenContract = CustomTokenFactory.connect(token, signer);
const tx = await customTokenContract.functions.mint(decode(UInt(32), amount));
const receipt = await tx.wait();
if (!receipt.status) throw new Error('Failed to mint token.');
if (!receipt.status)
throw new RaidenError(ErrorCodes.RDN_MINT_FAILED, { transactionHash: tx.hash! });

return tx.hash as Hash;
}
Expand Down Expand Up @@ -996,7 +998,7 @@ export class Raiden {
depositAmount,
);
const approveReceipt = await approveTx.wait();
if (!approveReceipt.status) throw new Error('Approve transaction failed.');
if (!approveReceipt.status) throw new RaidenError(ErrorCodes.RDN_APPROVE_TRANSACTION_FAILED);

onChange?.({
type: EventTypes.APPROVED,
Expand All @@ -1011,7 +1013,7 @@ export class Raiden {
currentUDCBalance.add(depositAmount),
);
const depositReceipt = await depositTx.wait();
if (!depositReceipt.status) throw new Error('Deposit transaction failed.');
if (!depositReceipt.status) throw new RaidenError(ErrorCodes.RDN_DEPOSIT_TRANSACTION_FAILED);

onChange?.({
type: EventTypes.DEPOSITED,
Expand Down Expand Up @@ -1053,7 +1055,7 @@ export class Raiden {
const tx = await signer.sendTransaction({ to, value: bigNumberify(value) });
const receipt = await tx.wait();

if (!receipt.status) throw new Error('Failed to transfer balance');
if (!receipt.status) throw new RaidenError(ErrorCodes.RDN_TRANSFER_ONCHAIN_BALANCE_FAILED);
return tx.hash! as Hash;
}

Expand Down Expand Up @@ -1085,7 +1087,7 @@ export class Raiden {
const tx = await tokenContract.functions.transfer(to, bigNumberify(value));
const receipt = await tx.wait();

if (!receipt.status) throw new Error('Failed to transfer tokens');
if (!receipt.status) throw new RaidenError(ErrorCodes.RDN_TRANSFER_ONCHAIN_TOKENS_FAILED);
return tx.hash! as Hash;
}
}
Expand Down
Loading

0 comments on commit d5d245b

Please sign in to comment.