Skip to content

Commit

Permalink
Add multiple error codes
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasnatter committed Mar 26, 2024
1 parent a1bb2e5 commit f0cd1e0
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 28 deletions.
14 changes: 10 additions & 4 deletions packages/shared/src/node-apis/RpcClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import assert from 'assert';
/* eslint-disable max-classes-per-file */
import EventEmitter, { once } from 'events';
import { filter, firstValueFrom, Subject, timeout } from 'rxjs';
import type { Logger } from 'winston';
Expand All @@ -13,6 +13,8 @@ type RpcResponse =
| { id: number; result: unknown }
| { id: number; error: { code: number; message: string } };

export class RpcClientError extends Error {}

export default class RpcClient<
Req extends Record<string, z.ZodTypeAny>,
Res extends Record<string, z.ZodTypeAny>,
Expand Down Expand Up @@ -126,7 +128,7 @@ export default class RpcClient<
// if the socket closes after sending a request but before getting a
// response, we need to retry the request
once(this, DISCONNECT, { signal: controller.signal }).then(() => {
throw new Error('disconnected');
throw new RpcClientError('server disconnected before response was received');
}),
]);
controller.abort();
Expand All @@ -137,9 +139,13 @@ export default class RpcClient<
}
}

assert(response, 'no response received');
if (!response) {
throw new RpcClientError('no response received');
}

if ('error' in response) throw new Error(response.error.message);
if ('error' in response) {
throw new RpcClientError(response.error.message);
}

this.logger?.info(
`received response from rpc client: ${response} ${JSON.stringify(response)}}`,
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/rpc/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const validateSwapAmount = (
if (amount < minimumAmount) {
return {
success: false,
reason: `expected amount is below minimum swap amount (${minimumAmount})`,
reason: `given amount (${amount}) is below minimum swap amount (${minimumAmount})`,
};
}

Expand All @@ -22,7 +22,7 @@ export const validateSwapAmount = (
if (maxAmount != null && amount > maxAmount) {
return {
success: false,
reason: `expected amount is above maximum swap amount (${maxAmount})`,
reason: `given amount (${amount}) is above maximum swap amount (${maxAmount})`,
};
}

Expand Down
5 changes: 3 additions & 2 deletions packages/swap/src/handlers/openSwapDepositChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@ export default async function openSwapDepositChannel(
) {
if (!validateAddress(input.destChain, input.destAddress, env.CHAINFLIP_NETWORK)) {
throw ServiceError.badRequest(
'bad-request',
`Address "${input.destAddress}" is not a valid "${input.destChain}" address`,
);
}

if (await screenAddress(input.destAddress)) {
throw ServiceError.badRequest(`Address "${input.destAddress}" is sanctioned`);
throw ServiceError.badRequest('bad-request', `Address "${input.destAddress}" is sanctioned`);
}

const result = await validateSwapAmount(
{ asset: input.srcAsset, chain: input.srcChain },
BigInt(input.expectedDepositAmount),
);

if (!result.success) throw ServiceError.badRequest(result.reason);
if (!result.success) throw ServiceError.badRequest('invalid-amount', result.reason);

const {
address: depositAddress,
Expand Down
41 changes: 32 additions & 9 deletions packages/swap/src/routes/quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express from 'express';
import type { Server } from 'socket.io';
import { Asset, Assets, Chain, Chains, getInternalAsset } from '@/shared/enums';
import { bigintMin, getPipAmountFromAmount } from '@/shared/functions';
import { RpcClientError } from '@/shared/node-apis/RpcClient';
import { quoteQuerySchema, SwapFee } from '@/shared/schemas';
import { calculateIncludedSwapFees, estimateIngressEgressFeeAssetAmount } from '@/swap/utils/fees';
import { getPools } from '@/swap/utils/pools';
Expand Down Expand Up @@ -55,14 +56,14 @@ const quote = (io: Server) => {
query: req.query,
error: queryResult.error,
});
throw ServiceError.badRequest('invalid request');
throw ServiceError.badRequest('bad-request', `the quote request parameters are invalid`);
}

logger.info('received a quote request', { query: req.query });

// detect if ingress and egress fees are exposed as gas asset amount or fee asset amount
// https://github.com/chainflip-io/chainflip-backend/pull/4497
// TODO: remove this once all networks are upraded to 1.3
// TODO: remove this once all networks are upgraded to 1.3
const ingressEgressFeeIsGasAssetAmount =
(await getIngressFee({ chain: 'Ethereum', asset: 'FLIP' })) ===
(await getIngressFee({ chain: 'Ethereum', asset: 'USDC' }));
Expand All @@ -74,7 +75,7 @@ const quote = (io: Server) => {
const amountResult = await validateSwapAmount(srcChainAsset, BigInt(query.amount));

if (!amountResult.success) {
throw ServiceError.badRequest(amountResult.reason);
throw ServiceError.badRequest('invalid-amount', amountResult.reason);
}

const includedFees: SwapFee[] = [];
Expand All @@ -95,6 +96,7 @@ const quote = (io: Server) => {
let ingressFee = await getIngressFee(srcChainAsset);
if (ingressFee == null) {
throw ServiceError.internalError(
'rpc-error',
`could not determine ingress fee for ${getInternalAsset(srcChainAsset)}`,
);
}
Expand All @@ -112,7 +114,10 @@ const quote = (io: Server) => {
});
swapInputAmount -= ingressFee;
if (swapInputAmount <= 0n) {
throw ServiceError.badRequest(`amount is lower than estimated ingress fee (${ingressFee})`);
throw ServiceError.badRequest(
'invalid-amount',
`deposit amount (${query.amount}) is lower than estimated ingress fee (${ingressFee})`,
);
}

if (query.brokerCommissionBps) {
Expand Down Expand Up @@ -170,6 +175,7 @@ const quote = (io: Server) => {
let egressFee = await getEgressFee(destChainAsset);
if (egressFee == null) {
throw ServiceError.internalError(
'rpc-error',
`could not determine egress fee for ${getInternalAsset(destChainAsset)}`,
);
}
Expand All @@ -193,7 +199,8 @@ const quote = (io: Server) => {

if (egressAmount < minimumEgressAmount) {
throw ServiceError.badRequest(
`egress amount (${egressAmount}) is lower than minimum egress amount (${minimumEgressAmount})`,
'invalid-amount',
`expected egress amount (${egressAmount}) is lower than minimum egress amount (${minimumEgressAmount})`,
);
}

Expand Down Expand Up @@ -223,13 +230,29 @@ const quote = (io: Server) => {
} catch (err) {
if (err instanceof ServiceError) throw err;

const message =
err instanceof Error ? err.message : 'unknown error (possibly no liquidity)';
let httpCode = 500;
let errorPayload;
if (err instanceof RpcClientError) {
if (err.message.includes('InsufficientLiquidity')) {
httpCode = 400;
errorPayload = {
code: 'invalid-amount',
message: `insufficient liquidity for swapping deposit amount (${query.amount})`,
error: err.message,
};
} else {
errorPayload = { code: 'rpc-error', message: err.message, error: err.message };
}
} else {
const message =
err instanceof Error ? err.message : 'an unexpected internal error occurred';
errorPayload = { code: 'rpc-error', message, error: message };
}

logger.error('error while collecting quotes:', err);

// DEPRECATED(1.3): remove `error`
res.status(500).json({ message, error: message });
// DEPRECATED(1.3): remove `error`, return ServiceError.internalError instead
res.status(httpCode).json(errorPayload);
}
}),
);
Expand Down
11 changes: 8 additions & 3 deletions packages/swap/src/routes/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const coerceChain = (chain: string) => {
case 'POLKADOT':
return screamingSnakeToPascalCase(uppercase);
default:
throw ServiceError.badRequest(`invalid chain "${chain}"`);
throw ServiceError.badRequest('bad-request', `invalid chain "${chain}"`);
}
};

Expand Down Expand Up @@ -126,7 +126,12 @@ router.get(
}
}

ServiceError.assert(swapDepositChannel || swap || failedSwap, 'notFound', 'resource not found');
ServiceError.assert(
swapDepositChannel || swap || failedSwap,
'notFound',
'not-found',
`no swap for id "${id}" found`,
);

let state: State;
let failureMode;
Expand Down Expand Up @@ -268,7 +273,7 @@ router.post(
const result = openSwapDepositChannelSchema.safeParse(req.body);
if (!result.success) {
logger.info('received bad request for new swap', { body: req.body });
throw ServiceError.badRequest('invalid request body');
throw ServiceError.badRequest('bad-request', 'invalid request payload');
}

const { srcChainExpiryBlock, channelOpeningFee, ...response } = await openSwapDepositChannel(
Expand Down
6 changes: 3 additions & 3 deletions packages/swap/src/routes/thirdPartySwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ router.post(
logger.info('received bad request for new third party swap', {
body: req.body,
});
throw ServiceError.badRequest('invalid request body');
throw ServiceError.badRequest('bad-request', 'invalid request payload');
}
try {
await prisma.thirdPartySwap.create({
Expand All @@ -31,7 +31,7 @@ router.post(
});
res.sendStatus(201);
} catch (err) {
if (err instanceof Error) throw ServiceError.internalError(err.message);
if (err instanceof Error) throw ServiceError.internalError('internal-error', err.message);
throw ServiceError.internalError();
}
}),
Expand All @@ -54,7 +54,7 @@ router.get(
res.json({ ...swap });
} catch (err) {
if (err instanceof ServiceError) throw err;
if (err instanceof Error) throw ServiceError.internalError(err.message);
if (err instanceof Error) throw ServiceError.internalError('internal-error', err.message);
throw ServiceError.internalError();
}
}),
Expand Down
21 changes: 16 additions & 5 deletions packages/swap/src/utils/ServiceError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,41 @@ type IgnoredField =
| 'prepareStackTrace'
| 'stackTraceLimit';

type ErrorCode = 'not-found' | 'bad-request' | 'internal-error' | 'rpc-error' | 'invalid-amount';

export default class ServiceError extends Error {
static badRequest(code: string, message: string): ServiceError {
static badRequest(
code: ErrorCode = 'bad-request',
message = 'the request payload is not valid',
): ServiceError {
return new ServiceError(code, message, 400);
}

static notFound(code: string, message = 'resource not found'): ServiceError {
static notFound(
code: ErrorCode = 'not-found',
message = 'the requested resource was not found',
): ServiceError {
return new ServiceError(code, message, 404);
}

static internalError(code: string, message = 'internal error'): ServiceError {
static internalError(
code: ErrorCode = 'internal-error',
message = 'an unexpected internal error occurred',
): ServiceError {
return new ServiceError(code, message, 500);
}

static assert(
condition: unknown,
type: Exclude<keyof typeof ServiceError, IgnoredField>,
code: string,
code: ErrorCode,
message: string,
): asserts condition {
if (!condition) throw ServiceError[type](code, message);
}

constructor(
readonly code: string,
readonly code: ErrorCode,
message: string,
readonly httpCode: number,
) {
Expand Down

0 comments on commit f0cd1e0

Please sign in to comment.