Skip to content

Commit edefe59

Browse files
petertonysmith94Torres-ssfarboleya
authored
chore: prepend version mismatch to GraphQL errors (#3580)
* chore: refactored the gql error handler * chore: moved incompatible node version from instantiation to error * chore: add error to the subscription * chore: changeset --------- Co-authored-by: Sérgio Torres <[email protected]> Co-authored-by: Anderson Arboleya <[email protected]>
1 parent 60caa27 commit edefe59

File tree

7 files changed

+217
-80
lines changed

7 files changed

+217
-80
lines changed

.changeset/angry-crabs-float.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fuel-ts/account": patch
3+
---
4+
5+
chore: prepend version mismatch to GraphQL errors

packages/account/src/providers/fuel-graphql-subscriber.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { ErrorCode, FuelError } from '@fuel-ts/errors';
22
import type { DocumentNode } from 'graphql';
33
import { print } from 'graphql';
44

5+
import { assertGqlResponseHasNoErrors } from './utils/handle-gql-error-message';
6+
57
type FuelGraphQLSubscriberOptions = {
68
url: string;
79
query: DocumentNode;
@@ -10,6 +12,7 @@ type FuelGraphQLSubscriberOptions = {
1012
};
1113

1214
export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
15+
public static incompatibleNodeVersionMessage: string | false = false;
1316
private static textDecoder = new TextDecoder();
1417

1518
private constructor(private stream: ReadableStreamDefaultReader<Uint8Array>) {}
@@ -50,12 +53,7 @@ export class FuelGraphqlSubscriber implements AsyncIterator<unknown> {
5053
if (this.events.length > 0) {
5154
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5255
const { data, errors } = this.events.shift()!;
53-
if (Array.isArray(errors)) {
54-
throw new FuelError(
55-
FuelError.CODES.INVALID_REQUEST,
56-
errors.map((err) => err.message).join('\n\n')
57-
);
58-
}
56+
assertGqlResponseHasNoErrors(errors, FuelGraphqlSubscriber.incompatibleNodeVersionMessage);
5957
return { value: data, done: false };
6058
}
6159
const { value, done } = await this.stream.read();

packages/account/src/providers/provider.test.ts

+108-49
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { InputType, OutputType, ReceiptType } from '@fuel-ts/transactions';
99
import { DateTime, arrayify, sleep } from '@fuel-ts/utils';
1010
import { ASSET_A, ASSET_B } from '@fuel-ts/utils/test-utils';
1111
import { versions } from '@fuel-ts/versions';
12-
import * as fuelTsVersionsMod from '@fuel-ts/versions';
1312

13+
import { Wallet } from '..';
1414
import {
1515
messageStatusResponse,
1616
MESSAGE_PROOF_RAW_RESPONSE,
@@ -20,6 +20,7 @@ import {
2020
MOCK_TX_UNKNOWN_RAW_PAYLOAD,
2121
MOCK_TX_SCRIPT_RAW_PAYLOAD,
2222
} from '../../test/fixtures/transaction-summary';
23+
import { mockIncompatibleVersions } from '../../test/utils/mockIncompabileVersions';
2324
import { setupTestProviderAndWallets, launchNode, TestMessage } from '../test-utils';
2425

2526
import type { Coin } from './coin';
@@ -1148,74 +1149,132 @@ describe('Provider', () => {
11481149
expect(gasConfig.maxGasPerTx).toBeDefined();
11491150
});
11501151

1151-
it('warns on difference between major client version and supported major version', async () => {
1152-
const { FUEL_CORE } = versions;
1153-
const [major, minor, patch] = FUEL_CORE.split('.');
1154-
const majorMismatch = major === '0' ? 1 : parseInt(patch, 10) - 1;
1152+
it('Prepend a warning to an error with version mismatch [major]', async () => {
1153+
const { current, supported } = mockIncompatibleVersions({
1154+
isMajorMismatch: true,
1155+
isMinorMismatch: false,
1156+
});
11551157

1156-
const mock = {
1157-
isMajorSupported: false,
1158-
isMinorSupported: true,
1159-
isPatchSupported: true,
1160-
supportedVersion: `${majorMismatch}.${minor}.${patch}`,
1161-
};
1158+
using launched = await setupTestProviderAndWallets();
1159+
const {
1160+
provider: { url },
1161+
} = launched;
11621162

1163-
if (mock.supportedVersion === FUEL_CORE) {
1164-
throw new Error();
1165-
}
1163+
const provider = await new Provider(url).init();
1164+
const sender = Wallet.generate({ provider });
1165+
const receiver = Wallet.generate({ provider });
11661166

1167-
const spy = vi.spyOn(fuelTsVersionsMod, 'checkFuelCoreVersionCompatibility');
1168-
spy.mockImplementationOnce(() => mock);
1167+
await expectToThrowFuelError(() => sender.transfer(receiver.address, 1), {
1168+
code: ErrorCode.NOT_ENOUGH_FUNDS,
1169+
message: [
1170+
`The account(s) sending the transaction don't have enough funds to cover the transaction.`,
1171+
``,
1172+
`The Fuel Node that you are trying to connect to is using fuel-core version ${current.FUEL_CORE}.`,
1173+
`The TS SDK currently supports fuel-core version ${supported.FUEL_CORE}.`,
1174+
`Things may not work as expected.`,
1175+
].join('\n'),
1176+
});
1177+
});
11691178

1170-
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1179+
it('Prepend a warning to an error with version mismatch [minor]', async () => {
1180+
const { current, supported } = mockIncompatibleVersions({
1181+
isMajorMismatch: false,
1182+
isMinorMismatch: true,
1183+
});
11711184

11721185
using launched = await setupTestProviderAndWallets();
1173-
const { provider } = launched;
1186+
const {
1187+
provider: { url },
1188+
} = launched;
11741189

1175-
await new Provider(provider.url).init();
1190+
const provider = await new Provider(url).init();
1191+
const sender = Wallet.generate({ provider });
1192+
const receiver = Wallet.generate({ provider });
11761193

1177-
expect(consoleWarnSpy).toHaveBeenCalledOnce();
1178-
expect(consoleWarnSpy).toHaveBeenCalledWith(
1179-
`The Fuel Node that you are trying to connect to is using fuel-core version ${FUEL_CORE},
1180-
which is not supported by the version of the TS SDK that you are using.
1181-
Things may not work as expected.
1182-
Supported fuel-core version: ${mock.supportedVersion}.`
1183-
);
1194+
await expectToThrowFuelError(() => sender.transfer(receiver.address, 1), {
1195+
code: ErrorCode.NOT_ENOUGH_FUNDS,
1196+
message: [
1197+
`The account(s) sending the transaction don't have enough funds to cover the transaction.`,
1198+
``,
1199+
`The Fuel Node that you are trying to connect to is using fuel-core version ${current.FUEL_CORE}.`,
1200+
`The TS SDK currently supports fuel-core version ${supported.FUEL_CORE}.`,
1201+
`Things may not work as expected.`,
1202+
].join('\n'),
1203+
});
11841204
});
11851205

1186-
it('warns on difference between minor client version and supported minor version', async () => {
1187-
const { FUEL_CORE } = versions;
1188-
const [major, minor, patch] = FUEL_CORE.split('.');
1189-
const minorMismatch = minor === '0' ? 1 : parseInt(patch, 10) - 1;
1206+
it('Prepend a warning to a subscription error with version mismatch [major]', async () => {
1207+
const { current, supported } = mockIncompatibleVersions({
1208+
isMajorMismatch: true,
1209+
isMinorMismatch: false,
1210+
});
11901211

1191-
const mock = {
1192-
isMajorSupported: true,
1193-
isMinorSupported: false,
1194-
isPatchSupported: true,
1195-
supportedVersion: `${major}.${minorMismatch}.${patch}`,
1196-
};
1212+
using launched = await setupTestProviderAndWallets();
1213+
const { provider } = launched;
11971214

1198-
if (mock.supportedVersion === FUEL_CORE) {
1199-
throw new Error();
1200-
}
1215+
await expectToThrowFuelError(
1216+
async () => {
1217+
for await (const value of await provider.operations.statusChange({
1218+
transactionId: 'invalid transaction id',
1219+
})) {
1220+
// shouldn't be reached and should fail if reached
1221+
expect(value).toBeFalsy();
1222+
}
1223+
},
12011224

1202-
const spy = vi.spyOn(fuelTsVersionsMod, 'checkFuelCoreVersionCompatibility');
1203-
spy.mockImplementationOnce(() => mock);
1225+
{ code: FuelError.CODES.INVALID_REQUEST }
1226+
);
12041227

1205-
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1228+
const chainId = await provider.getChainId();
1229+
const response = new TransactionResponse('invalid transaction id', provider, chainId);
1230+
1231+
await expectToThrowFuelError(() => response.waitForResult(), {
1232+
code: FuelError.CODES.INVALID_REQUEST,
1233+
message: [
1234+
`Failed to parse "TransactionId": Invalid character 'i' at position 0`,
1235+
``,
1236+
`The Fuel Node that you are trying to connect to is using fuel-core version ${current.FUEL_CORE}.`,
1237+
`The TS SDK currently supports fuel-core version ${supported.FUEL_CORE}.`,
1238+
`Things may not work as expected.`,
1239+
].join('\n'),
1240+
});
1241+
});
1242+
1243+
it('Prepend a warning to a subscription error with version mismatch [minor]', async () => {
1244+
const { current, supported } = mockIncompatibleVersions({
1245+
isMajorMismatch: false,
1246+
isMinorMismatch: true,
1247+
});
12061248

12071249
using launched = await setupTestProviderAndWallets();
12081250
const { provider } = launched;
12091251

1210-
await new Provider(provider.url).init();
1252+
await expectToThrowFuelError(
1253+
async () => {
1254+
for await (const value of await provider.operations.statusChange({
1255+
transactionId: 'invalid transaction id',
1256+
})) {
1257+
// shouldn't be reached and should fail if reached
1258+
expect(value).toBeFalsy();
1259+
}
1260+
},
12111261

1212-
expect(consoleWarnSpy).toHaveBeenCalledOnce();
1213-
expect(consoleWarnSpy).toHaveBeenCalledWith(
1214-
`The Fuel Node that you are trying to connect to is using fuel-core version ${FUEL_CORE},
1215-
which is not supported by the version of the TS SDK that you are using.
1216-
Things may not work as expected.
1217-
Supported fuel-core version: ${mock.supportedVersion}.`
1262+
{ code: FuelError.CODES.INVALID_REQUEST }
12181263
);
1264+
1265+
const chainId = await provider.getChainId();
1266+
const response = new TransactionResponse('invalid transaction id', provider, chainId);
1267+
1268+
await expectToThrowFuelError(() => response.waitForResult(), {
1269+
code: FuelError.CODES.INVALID_REQUEST,
1270+
message: [
1271+
`Failed to parse "TransactionId": Invalid character 'i' at position 0`,
1272+
``,
1273+
`The Fuel Node that you are trying to connect to is using fuel-core version ${current.FUEL_CORE}.`,
1274+
`The TS SDK currently supports fuel-core version ${supported.FUEL_CORE}.`,
1275+
`Things may not work as expected.`,
1276+
].join('\n'),
1277+
});
12191278
});
12201279

12211280
it('An invalid subscription request throws a FuelError and does not hold the test runner (closes all handles)', async () => {

packages/account/src/providers/provider.ts

+16-16
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import {
6262
} from './utils';
6363
import type { RetryOptions } from './utils/auto-retry-fetch';
6464
import { autoRetryFetch } from './utils/auto-retry-fetch';
65-
import { handleGqlErrorMessage } from './utils/handle-gql-error-message';
65+
import { assertGqlResponseHasNoErrors } from './utils/handle-gql-error-message';
6666
import { validatePaginationArgs } from './utils/validate-pagination-args';
6767

6868
const MAX_RETRIES = 10;
@@ -414,6 +414,8 @@ export default class Provider {
414414
private static chainInfoCache: ChainInfoCache = {};
415415
/** @hidden */
416416
private static nodeInfoCache: NodeInfoCache = {};
417+
/** @hidden */
418+
private static incompatibleNodeVersionMessage: string = '';
417419

418420
/** @hidden */
419421
public consensusParametersTimestamp?: number;
@@ -609,7 +611,7 @@ export default class Provider {
609611
vmBacktrace: data.nodeInfo.vmBacktrace,
610612
};
611613

612-
Provider.ensureClientVersionIsSupported(nodeInfo);
614+
Provider.setIncompatibleNodeVersionMessage(nodeInfo);
613615

614616
chain = processGqlChain(data.chain);
615617

@@ -628,18 +630,18 @@ export default class Provider {
628630
/**
629631
* @hidden
630632
*/
631-
private static ensureClientVersionIsSupported(nodeInfo: NodeInfo) {
633+
private static setIncompatibleNodeVersionMessage(nodeInfo: NodeInfo) {
632634
const { isMajorSupported, isMinorSupported, supportedVersion } =
633635
checkFuelCoreVersionCompatibility(nodeInfo.nodeVersion);
634636

635637
if (!isMajorSupported || !isMinorSupported) {
636-
// eslint-disable-next-line no-console
637-
console.warn(
638-
`The Fuel Node that you are trying to connect to is using fuel-core version ${nodeInfo.nodeVersion},
639-
which is not supported by the version of the TS SDK that you are using.
640-
Things may not work as expected.
641-
Supported fuel-core version: ${supportedVersion}.`
642-
);
638+
Provider.incompatibleNodeVersionMessage = [
639+
`The Fuel Node that you are trying to connect to is using fuel-core version ${nodeInfo.nodeVersion}.`,
640+
`The TS SDK currently supports fuel-core version ${supportedVersion}.`,
641+
`Things may not work as expected.`,
642+
].join('\n');
643+
FuelGraphqlSubscriber.incompatibleNodeVersionMessage =
644+
Provider.incompatibleNodeVersionMessage;
643645
}
644646
}
645647

@@ -657,12 +659,10 @@ Supported fuel-core version: ${supportedVersion}.`
657659
responseMiddleware: (response: GraphQLClientResponse<unknown> | Error) => {
658660
if ('response' in response) {
659661
const graphQlResponse = response.response as GraphQLResponse;
660-
661-
if (Array.isArray(graphQlResponse?.errors)) {
662-
for (const error of graphQlResponse.errors) {
663-
handleGqlErrorMessage(error.message, error);
664-
}
665-
}
662+
assertGqlResponseHasNoErrors(
663+
graphQlResponse.errors,
664+
Provider.incompatibleNodeVersionMessage
665+
);
666666
}
667667
},
668668
});

packages/account/src/providers/utils/handle-gql-error-message.ts

+45-7
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,61 @@ export enum GqlErrorMessage {
66
MAX_COINS_REACHED = 'max number of coins is reached while trying to fit the target',
77
}
88

9-
export const handleGqlErrorMessage = (errorMessage: string, rawError: GraphQLError) => {
10-
switch (errorMessage) {
9+
type GqlError = { message: string } | GraphQLError;
10+
11+
const mapGqlErrorMessage = (error: GqlError): FuelError => {
12+
switch (error.message) {
1113
case GqlErrorMessage.NOT_ENOUGH_COINS:
12-
throw new FuelError(
14+
return new FuelError(
1315
ErrorCode.NOT_ENOUGH_FUNDS,
1416
`The account(s) sending the transaction don't have enough funds to cover the transaction.`,
1517
{},
16-
rawError
18+
error
1719
);
1820
case GqlErrorMessage.MAX_COINS_REACHED:
19-
throw new FuelError(
21+
return new FuelError(
2022
ErrorCode.MAX_COINS_REACHED,
2123
'The account retrieving coins has exceeded the maximum number of coins per asset. Please consider combining your coins into a single UTXO.',
2224
{},
23-
rawError
25+
error
2426
);
2527
default:
26-
throw new FuelError(ErrorCode.INVALID_REQUEST, errorMessage);
28+
return new FuelError(ErrorCode.INVALID_REQUEST, error.message, {}, error);
29+
}
30+
};
31+
32+
const mapGqlErrorWithIncompatibleNodeVersion = (
33+
error: FuelError,
34+
incompatibleNodeVersionMessage: string | false
35+
) => {
36+
if (!incompatibleNodeVersionMessage) {
37+
return error;
2738
}
39+
40+
return new FuelError(
41+
error.code,
42+
`${error.message}\n\n${incompatibleNodeVersionMessage}`,
43+
error.metadata,
44+
error.rawError
45+
);
46+
};
47+
48+
export const assertGqlResponseHasNoErrors = (
49+
errors: GqlError[] | undefined,
50+
incompatibleNodeVersionMessage: string | false = false
51+
) => {
52+
if (!Array.isArray(errors)) {
53+
return;
54+
}
55+
56+
const mappedErrors = errors.map(mapGqlErrorMessage);
57+
if (mappedErrors.length === 1) {
58+
throw mapGqlErrorWithIncompatibleNodeVersion(mappedErrors[0], incompatibleNodeVersionMessage);
59+
}
60+
61+
const errorMessage = mappedErrors.map((err) => err.message).join('\n');
62+
throw mapGqlErrorWithIncompatibleNodeVersion(
63+
new FuelError(ErrorCode.INVALID_REQUEST, errorMessage, {}, mappedErrors),
64+
incompatibleNodeVersionMessage
65+
);
2866
};

0 commit comments

Comments
 (0)