Skip to content

Commit 9cb8149

Browse files
authored
feat(wallet): Gracefully handle 422 guardian errors for native clients (#2799)
1 parent e7838e1 commit 9cb8149

3 files changed

Lines changed: 163 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { AxiosError, AxiosResponse } from 'axios';
2+
import GuardianClient from './index';
3+
import { JsonRpcError, RpcErrorCode } from '../zkEvm/JsonRpcError';
4+
import { WalletConfiguration } from '../config';
5+
import { MetaTransaction } from '../zkEvm/types';
6+
7+
// Minimal stubs so the constructor doesn't fail
8+
jest.mock('../confirmation/confirmation', () => ({
9+
__esModule: true,
10+
default: jest.fn().mockImplementation(() => ({
11+
loading: jest.fn(),
12+
closeWindow: jest.fn(),
13+
})),
14+
}));
15+
16+
const mockUser = {
17+
zkEvm: { ethAddress: '0xabc', userAdminKey: '0xkey' },
18+
accessToken: 'test-token',
19+
};
20+
21+
const mockGetUser = jest.fn().mockResolvedValue(mockUser);
22+
23+
const mockEvaluateTransaction = jest.fn();
24+
const mockGuardianApi = {
25+
evaluateTransaction: mockEvaluateTransaction,
26+
} as any;
27+
28+
const baseConfig = {
29+
crossSdkBridgeEnabled: false,
30+
} as unknown as WalletConfiguration;
31+
32+
const metaTransactions: MetaTransaction[] = [
33+
{
34+
to: '0xcontract',
35+
data: '0x1234',
36+
value: BigInt(0),
37+
delegateCall: false,
38+
revertOnError: true,
39+
},
40+
];
41+
42+
const evalParams = {
43+
chainId: 'eip155:13372',
44+
nonce: '1',
45+
metaTransactions,
46+
};
47+
48+
function makeAxiosError(status: number, data?: unknown): AxiosError {
49+
const error = new AxiosError('Request failed');
50+
error.response = {
51+
status,
52+
data,
53+
headers: {},
54+
config: {} as any,
55+
statusText: '',
56+
} as AxiosResponse;
57+
return error;
58+
}
59+
60+
function makeClient(crossSdkBridgeEnabled: boolean): GuardianClient {
61+
return new GuardianClient({
62+
config: { ...baseConfig, crossSdkBridgeEnabled } as WalletConfiguration,
63+
getUser: mockGetUser,
64+
guardianApi: mockGuardianApi,
65+
passportDomain: 'https://passport.immutable.com',
66+
clientId: 'test-client',
67+
});
68+
}
69+
70+
describe('GuardianClient.evaluateEVMTransaction — 422 handling', () => {
71+
describe('when Guardian returns 422 and crossSdkBridgeEnabled is true (game bridge)', () => {
72+
it('throws TRANSACTION_REVERTED with the revert reason from the response', async () => {
73+
mockEvaluateTransaction.mockRejectedValue(
74+
makeAxiosError(422, { message: 'execution reverted: ERC20: transfer amount exceeds balance' }),
75+
);
76+
77+
const client = makeClient(true);
78+
79+
await expect(
80+
(client as any).evaluateEVMTransaction(evalParams),
81+
).rejects.toMatchObject({
82+
code: RpcErrorCode.TRANSACTION_REVERTED,
83+
message: expect.stringContaining('execution reverted: ERC20: transfer amount exceeds balance'),
84+
});
85+
});
86+
87+
it('throws TRANSACTION_REVERTED with a fallback message when the response has no message field', async () => {
88+
mockEvaluateTransaction.mockRejectedValue(makeAxiosError(422, {}));
89+
90+
const client = makeClient(true);
91+
92+
await expect(
93+
(client as any).evaluateEVMTransaction(evalParams),
94+
).rejects.toMatchObject({
95+
code: RpcErrorCode.TRANSACTION_REVERTED,
96+
message: expect.stringContaining('Transaction will revert:'),
97+
});
98+
});
99+
100+
it('throws a JsonRpcError instance', async () => {
101+
mockEvaluateTransaction.mockRejectedValue(
102+
makeAxiosError(422, { message: 'reverted' }),
103+
);
104+
105+
const client = makeClient(true);
106+
107+
await expect(
108+
(client as any).evaluateEVMTransaction(evalParams),
109+
).rejects.toBeInstanceOf(JsonRpcError);
110+
});
111+
});
112+
113+
describe('when Guardian returns 422 and crossSdkBridgeEnabled is false (web SDK)', () => {
114+
it('returns confirmationRequired: true instead of throwing', async () => {
115+
mockEvaluateTransaction.mockRejectedValue(
116+
makeAxiosError(422, { message: 'execution reverted' }),
117+
);
118+
119+
const client = makeClient(false);
120+
121+
const result = await (client as any).evaluateEVMTransaction(evalParams);
122+
123+
expect(result).toEqual({ confirmationRequired: true });
124+
});
125+
});
126+
127+
describe('when Guardian returns 403', () => {
128+
it('throws SERVICE_UNAVAILABLE_ERROR regardless of bridge flag', async () => {
129+
mockEvaluateTransaction.mockRejectedValue(makeAxiosError(403));
130+
131+
const client = makeClient(false);
132+
133+
await expect(
134+
(client as any).evaluateEVMTransaction(evalParams),
135+
).rejects.toMatchObject({ type: 'SERVICE_UNAVAILABLE_ERROR' });
136+
});
137+
});
138+
139+
describe('when Guardian returns an unexpected error', () => {
140+
it('throws INTERNAL_ERROR', async () => {
141+
mockEvaluateTransaction.mockRejectedValue(new Error('network timeout'));
142+
143+
const client = makeClient(false);
144+
145+
await expect(
146+
(client as any).evaluateEVMTransaction(evalParams),
147+
).rejects.toMatchObject({ code: RpcErrorCode.INTERNAL_ERROR });
148+
});
149+
});
150+
});

packages/wallet/src/guardian/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ export default class GuardianClient {
173173
throw new WalletError('Service unavailable', WalletErrorType.SERVICE_UNAVAILABLE_ERROR);
174174
}
175175

176+
if (isAxiosError(error) && error.response?.status === 422) {
177+
if (this.crossSdkBridgeEnabled) {
178+
const revertReason = (error.response?.data as any)?.message ?? 'A transaction simulation reverted';
179+
throw new JsonRpcError(
180+
RpcErrorCode.TRANSACTION_REVERTED,
181+
`Transaction will revert: ${revertReason}`,
182+
);
183+
}
184+
185+
return { confirmationRequired: true };
186+
}
187+
176188
const errorMessage = error instanceof Error ? error.message : String(error);
177189
throw new JsonRpcError(
178190
RpcErrorCode.INTERNAL_ERROR,

packages/wallet/src/zkEvm/JsonRpcError.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum ProviderErrorCode {
1212

1313
export enum RpcErrorCode {
1414
RPC_SERVER_ERROR = -32000,
15+
TRANSACTION_REVERTED = -32015,
1516
INVALID_REQUEST = -32600,
1617
METHOD_NOT_FOUND = -32601,
1718
INVALID_PARAMS = -32602,

0 commit comments

Comments
 (0)