Skip to content

Commit b89bea1

Browse files
jlobue10claude
andauthored
Stop daemon broadcast events from rejecting pending wallet_ui requests (#3012)
The main-process sendCommand socket registers as service wallet_ui, so the daemon forwards every broadcast event to it (farming_info, get_fee_estimate, get_connections, ...). Its message handler parsed each frame with json-bigint's useNativeBigInt parser before checking request_id, and that parser passes any number literal longer than 15 characters to BigInt() even when it contains a decimal point. A harvester farming_info lookup time such as 1.7656183690123726 therefore threw "Cannot convert 1.7656183690123726 to a BigInt", and the catch block rejected whichever request happened to be pending. Because get_offer_summary runs inside the take_offer confirmation flow, any farm with slow-enough plot lookups hits this race on nearly every offer accept: the user sees the BigInt error dialog and the offer command is never sent to the wallet. Parse incoming frames with the native-bigint parser first, fall back to the BigNumber parser for frames carrying long-precision floats, and ignore frames that are not valid JSON instead of rejecting the pending request. Matched responses keep their existing native-bigint behavior. Claude-Session: https://claude.ai/code/session_016jfMfuGTvCmMyL7EM92dmj Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent be3a41c commit b89bea1

2 files changed

Lines changed: 95 additions & 10 deletions

File tree

packages/gui/src/electron/api/sendCommand.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,71 @@ describe('sendCommand', () => {
281281
expect(rawMessage).not.toContain('9007199254740993n');
282282
});
283283

284+
it('ignores broadcast events with long-precision floats while a command is pending', async () => {
285+
await startServer((socket, message) => {
286+
if (message.command === 'register_service') {
287+
socket.send(JSONBig.stringify({ request_id: message.request_id, data: { success: true } }));
288+
return;
289+
}
290+
291+
// Raw frame mimicking a chia_harvester farming_info broadcast: the
292+
// lookup time float is longer than 15 characters, which used to make
293+
// the useNativeBigInt parser throw and reject the pending request.
294+
socket.send(
295+
'{"ack":false,"command":"farming_info","data":{"challenge_hash":"0xabc","total_plots":1000,"time":1.7656183690123726},"destination":"wallet_ui","origin":"chia_harvester","request_id":"broadcast"}',
296+
);
297+
socket.send(JSONBig.stringify({ request_id: message.request_id, data: { success: true, value: 'done' } }));
298+
});
299+
const sendCommand = loadSendCommand();
300+
301+
await expect(sendCommand('get_offer_summary', 'chia_wallet')).resolves.toMatchObject({ value: 'done' });
302+
});
303+
304+
it('resolves a matched response that contains long-precision floats', async () => {
305+
await startServer((socket, message) => {
306+
if (message.command === 'register_service') {
307+
socket.send(JSONBig.stringify({ request_id: message.request_id, data: { success: true } }));
308+
return;
309+
}
310+
311+
socket.send(`{"request_id":"${message.request_id}","data":{"success":true,"fee_rate":1.7656183690123726}}`);
312+
});
313+
const sendCommand = loadSendCommand();
314+
315+
const response = await sendCommand<{ fee_rate: unknown }>('get_fee_estimate', 'chia_full_node');
316+
317+
expect(String(response.fee_rate)).toBe('1.7656183690123726');
318+
});
319+
320+
it('ignores frames that are not valid JSON while a command is pending', async () => {
321+
await startServer((socket, message) => {
322+
if (message.command === 'register_service') {
323+
socket.send(JSONBig.stringify({ request_id: message.request_id, data: { success: true } }));
324+
return;
325+
}
326+
327+
socket.send('not json');
328+
socket.send(JSONBig.stringify({ request_id: message.request_id, data: { success: true, value: 'done' } }));
329+
});
330+
const sendCommand = loadSendCommand();
331+
332+
await expect(sendCommand('get_wallets', 'chia_wallet')).resolves.toMatchObject({ value: 'done' });
333+
});
334+
335+
it('ignores broadcast events with long-precision floats during service registration', async () => {
336+
await startServer((socket, message) => {
337+
if (message.command === 'register_service') {
338+
socket.send(
339+
'{"ack":false,"command":"farming_info","data":{"time":1.2395747610135004},"destination":"wallet_ui","origin":"chia_harvester","request_id":"broadcast"}',
340+
);
341+
}
342+
socket.send(JSONBig.stringify({ request_id: message.request_id, data: { success: true, value: 'done' } }));
343+
});
344+
const sendCommand = loadSendCommand();
345+
346+
await expect(sendCommand('get_network_info', 'chia_wallet')).resolves.toMatchObject({ value: 'done' });
347+
});
348+
284349
it('rejects daemon command errors', async () => {
285350
await startServer((socket, message) => {
286351
socket.send(

packages/gui/src/electron/api/sendCommand.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ import { loadConfig } from '../utils/loadConfig';
66
const REQUEST_TIMEOUT_MS = 10 * 60 * 1000;
77
const JSONBigNative = JSONBig({ useNativeBigInt: true });
88

9+
// The wallet_ui socket also receives daemon broadcast events (farming_info,
10+
// get_fee_estimate, get_connections, ...). json-bigint with useNativeBigInt
11+
// passes any number literal longer than 15 characters to BigInt(), which
12+
// throws on the long-precision floats those events carry, so fall back to the
13+
// BigNumber-based parser before giving up on a frame. Returns undefined for
14+
// frames that are not valid JSON at all.
15+
function parseIncomingMessage(data: Buffer): any | undefined {
16+
const text = data.toString();
17+
18+
try {
19+
return JSONBigNative.parse(text);
20+
} catch {
21+
try {
22+
return JSONBig.parse(text);
23+
} catch {
24+
return undefined;
25+
}
26+
}
27+
}
28+
929
let socketPromise: Promise<WebSocket> | undefined;
1030

1131
async function connect(): Promise<WebSocket> {
@@ -37,12 +57,12 @@ async function connect(): Promise<WebSocket> {
3757
});
3858

3959
function handleMessage(data: Buffer) {
40-
try {
41-
const response = JSONBigNative.parse(data.toString());
42-
if (response.request_id !== requestId) {
43-
return;
44-
}
60+
const response = parseIncomingMessage(data);
61+
if (!response || response.request_id !== requestId) {
62+
return;
63+
}
4564

65+
try {
4666
if (!response.data.success) {
4767
throw new Error(`Daemon service is not registered`);
4868
}
@@ -167,12 +187,12 @@ export async function sendCommand<TResponse extends Record<string, unknown>>(
167187
}
168188

169189
function handleMessage(data: Buffer) {
170-
try {
171-
const response = JSONBigNative.parse(data.toString());
172-
if (response.request_id !== requestId) {
173-
return;
174-
}
190+
const response = parseIncomingMessage(data);
191+
if (!response || response.request_id !== requestId) {
192+
return;
193+
}
175194

195+
try {
176196
if (!response.data.success) {
177197
throw new Error(response.data.error);
178198
}

0 commit comments

Comments
 (0)