Skip to content

Commit 28626f1

Browse files
authored
Merge pull request #193 from theforager/simulation-errors
fix: propagate ABCI errors instead of swallowing them
2 parents a2b4213 + 395d149 commit 28626f1

3 files changed

Lines changed: 149 additions & 5 deletions

File tree

packages/types/src/errors.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export enum ErrorCode {
66
PARSE_ERROR = "PARSE_ERROR",
77
INVALID_RESPONSE = "INVALID_RESPONSE",
88
SUBSCRIPTION_ERROR = "SUBSCRIPTION_ERROR",
9-
PROTOCOL_ERROR = "PROTOCOL_ERROR"
9+
PROTOCOL_ERROR = "PROTOCOL_ERROR",
10+
ABCI_ERROR = "ABCI_ERROR"
1011
}
1112

1213
export enum ErrorCategory {
@@ -62,4 +63,24 @@ export class SubscriptionError extends QueryClientError {
6263
export class ProtocolError extends QueryClientError {
6364
readonly code = ErrorCode.PROTOCOL_ERROR;
6465
readonly category = ErrorCategory.PROTOCOL;
66+
}
67+
68+
/**
69+
* Error thrown when an ABCI query returns a non-zero response code.
70+
* This indicates the chain application rejected the query (e.g., simulation failure,
71+
* invalid transaction, out of gas during execution).
72+
*/
73+
export class AbciError extends QueryClientError {
74+
readonly code = ErrorCode.ABCI_ERROR;
75+
readonly category = ErrorCategory.SERVER;
76+
77+
constructor(
78+
message: string,
79+
/** The ABCI response code (non-zero indicates error) */
80+
public readonly abciCode: number,
81+
/** The log message from the ABCI response */
82+
public readonly log: string
83+
) {
84+
super(message);
85+
}
6586
}

packages/utils/src/rpc.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { abciQuery } from './rpc';
2+
import { ProtocolError, AbciError } from '@interchainjs/types';
3+
4+
// Mock fetch globally
5+
const mockFetch = jest.fn();
6+
global.fetch = mockFetch as any;
7+
8+
describe('abciQuery', () => {
9+
const endpoint = { url: 'http://localhost:26657', headers: {} };
10+
const path = '/cosmos.tx.v1beta1.Service/Simulate';
11+
const data = new Uint8Array([1, 2, 3]);
12+
13+
beforeEach(() => {
14+
mockFetch.mockReset();
15+
});
16+
17+
it('should throw ProtocolError on JSON-RPC error', async () => {
18+
mockFetch.mockResolvedValue({
19+
json: () => Promise.resolve({
20+
error: { code: -32600, message: 'Invalid Request' },
21+
}),
22+
});
23+
24+
await expect(abciQuery(endpoint, path, data)).rejects.toThrow(ProtocolError);
25+
await expect(abciQuery(endpoint, path, data)).rejects.toThrow('JSON-RPC error');
26+
});
27+
28+
it('should throw AbciError on non-zero ABCI response code', async () => {
29+
mockFetch.mockResolvedValue({
30+
json: () => Promise.resolve({
31+
result: {
32+
response: {
33+
code: 11,
34+
log: 'out of gas in location: ReadFlat; gasWanted: 0, gasUsed: 1000',
35+
value: '',
36+
},
37+
},
38+
}),
39+
});
40+
41+
await expect(abciQuery(endpoint, path, data)).rejects.toThrow(AbciError);
42+
43+
try {
44+
await abciQuery(endpoint, path, data);
45+
} catch (e) {
46+
expect(e).toBeInstanceOf(AbciError);
47+
const abciError = e as AbciError;
48+
expect(abciError.abciCode).toBe(11);
49+
expect(abciError.log).toBe('out of gas in location: ReadFlat; gasWanted: 0, gasUsed: 1000');
50+
}
51+
});
52+
53+
it('should throw AbciError with generic message when ABCI error has no log', async () => {
54+
mockFetch.mockResolvedValue({
55+
json: () => Promise.resolve({
56+
result: {
57+
response: {
58+
code: 5,
59+
log: '',
60+
value: '',
61+
},
62+
},
63+
}),
64+
});
65+
66+
await expect(abciQuery(endpoint, path, data)).rejects.toThrow(AbciError);
67+
68+
try {
69+
await abciQuery(endpoint, path, data);
70+
} catch (e) {
71+
const abciError = e as AbciError;
72+
expect(abciError.abciCode).toBe(5);
73+
expect(abciError.log).toBe('Unknown error');
74+
}
75+
});
76+
77+
it('should return decoded value on success (code 0)', async () => {
78+
// Base64 encoded [4, 5, 6]
79+
const base64Value = 'BAUG';
80+
mockFetch.mockResolvedValue({
81+
json: () => Promise.resolve({
82+
result: {
83+
response: {
84+
code: 0,
85+
log: '',
86+
value: base64Value,
87+
},
88+
},
89+
}),
90+
});
91+
92+
const result = await abciQuery(endpoint, path, data);
93+
expect(result).toEqual(new Uint8Array([4, 5, 6]));
94+
});
95+
96+
it('should preserve instanceof Error for backward compatibility', async () => {
97+
mockFetch.mockResolvedValue({
98+
json: () => Promise.resolve({
99+
result: {
100+
response: {
101+
code: 11,
102+
log: 'test error',
103+
value: '',
104+
},
105+
},
106+
}),
107+
});
108+
109+
await expect(abciQuery(endpoint, path, data)).rejects.toThrow(Error);
110+
});
111+
});

packages/utils/src/rpc.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HttpEndpoint, Rpc } from '@interchainjs/types';
1+
import { HttpEndpoint, Rpc, ProtocolError, AbciError, ParseError } from '@interchainjs/types';
22
import { toHttpEndpoint } from './endpoint';
33
import { fromBase64, toBase64, toHex } from './encoding';
44
import { randomId } from './random';
@@ -48,13 +48,25 @@ export async function abciQuery(
4848
const resp = await fetch(endpoint.url, req);
4949
const json = await resp.json();
5050
if (json['error'] != void 0) {
51-
throw new Error(`Request Error: ${json['error']}`);
51+
throw new ProtocolError(`JSON-RPC error: ${JSON.stringify(json['error'])}`);
5252
}
53+
const response = json['result']['response'];
54+
55+
// Check ABCI response code - non-zero indicates an error
56+
if (response['code'] !== 0) {
57+
const log = response['log'] || 'Unknown error';
58+
throw new AbciError(
59+
`ABCI query failed (code ${response['code']}): ${log}`,
60+
response['code'],
61+
log
62+
);
63+
}
64+
5365
try {
54-
const result = fromBase64(json['result']['response']['value']);
66+
const result = fromBase64(response['value']);
5567
return result;
5668
} catch (error) {
57-
throw new Error(`Request Error: ${json['result']['response']['log']}`);
69+
throw new ParseError(`Failed to decode ABCI response: ${response['log'] || 'invalid base64'}`);
5870
}
5971
}
6072

0 commit comments

Comments
 (0)