Skip to content

Commit 8353b5c

Browse files
committed
fix(stores,stores-eth): force-register on imperative fetch + surface per-request batch errors
- Imperative ERC20 fetch() outside a reactive observer no longer falls through: the Child now force-adds/removes the contract around waitFreshResponse so post-send refresh paths actually issue an eth_call even when nothing is observing balance/response yet. - ObservableJsonRpcBatchQuery exposes per-request errors via a new perRequestErrors map. Individual failures inside a batch response were previously dropped silently, leaving bad contract addresses stuck at ready(false) with no error signal. BatchParent.getError checks this map so per-contract error propagation works for both HTTP-level and per-request failures.
1 parent 43e03b0 commit 8353b5c

3 files changed

Lines changed: 51 additions & 4 deletions

File tree

packages/stores-eth/src/queries/erc20-balance-batch.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,27 @@ export class ObservableQueryEthereumERC20BalancesBatchParent {
7070
}
7171

7272
// Per-contract error: surface only the error of the chunk that owns this
73-
// contract, so a failing chunk doesn't contaminate siblings in other chunks.
73+
// contract, plus any per-request error the batch returned for it. A failing
74+
// chunk no longer contaminates siblings in other chunks.
7475
getError(contract: string): QueryError<unknown> | undefined {
7576
const key = contract.toLowerCase();
7677
const sortedKeys = Array.from(this.refcount.keys()).sort();
7778
const idx = sortedKeys.indexOf(key);
7879
if (idx === -1) return undefined;
7980
const chunkIdx = Math.floor(idx / BATCH_CHUNK_SIZE);
80-
return this.batchQueries[chunkIdx]?.error;
81+
const q = this.batchQueries[chunkIdx];
82+
if (!q) return undefined;
83+
if (q.error) return q.error;
84+
const perReq = q.perRequestErrors[key];
85+
if (perReq) {
86+
return {
87+
status: 0,
88+
statusText: "batch-request-error",
89+
message: perReq.message,
90+
data: perReq as unknown,
91+
};
92+
}
93+
return undefined;
8194
}
8295

8396
async waitFreshResponse(): Promise<void> {

packages/stores-eth/src/queries/erc20-balance.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,25 @@ export class ObservableQueryEthereumERC20BalanceImpl
108108
};
109109
}
110110

111+
protected async ensureFetched(): Promise<void> {
112+
// Force temporary registration so imperative callers (outside a reactive
113+
// observer) still trigger an actual eth_call.
114+
this.parent.addContract(this.contractAddress);
115+
try {
116+
await this.parent.waitFreshResponse();
117+
} finally {
118+
this.parent.removeContract(this.contractAddress);
119+
}
120+
}
121+
111122
fetch(): Promise<void> {
112-
return this.parent.waitFreshResponse();
123+
return this.ensureFetched();
113124
}
114125

115126
async waitFreshResponse(): Promise<
116127
Readonly<QueryResponse<unknown>> | undefined
117128
> {
118-
await this.parent.waitFreshResponse();
129+
await this.ensureFetched();
119130
return undefined;
120131
}
121132

packages/stores/src/common/query/json-rpc-batch.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,32 @@ import { simpleFetch } from "@keplr-wallet/simple-fetch";
44
import { JsonRpcResponse } from "@keplr-wallet/types";
55
import { Hash } from "@keplr-wallet/crypto";
66
import { Buffer } from "buffer/";
7+
import { makeObservable, observable, runInAction } from "mobx";
78

89
export interface JsonRpcBatchRequest {
910
method: string;
1011
params: unknown;
1112
id: string;
1213
}
1314

15+
export interface JsonRpcBatchRequestError {
16+
code: number;
17+
message: string;
18+
data?: unknown;
19+
}
20+
1421
/**
1522
* Observable query for batched JSON-RPC requests.
1623
* Manages an array of `JsonRpcBatchRequest` and returns a map of results keyed by request ID.
24+
* Per-request errors (when the batch HTTP call succeeds but individual calls fail)
25+
* are surfaced via `perRequestErrors`.
1726
*/
1827
export class ObservableJsonRpcBatchQuery<T = unknown> extends ObservableQuery<
1928
Record<string, T>
2029
> {
30+
@observable.ref
31+
protected _perRequestErrors: Record<string, JsonRpcBatchRequestError> = {};
32+
2133
constructor(
2234
sharedContext: QuerySharedContext,
2335
baseURL: string,
@@ -26,6 +38,11 @@ export class ObservableJsonRpcBatchQuery<T = unknown> extends ObservableQuery<
2638
options: Partial<QueryOptions> = {}
2739
) {
2840
super(sharedContext, baseURL, url, options);
41+
makeObservable(this);
42+
}
43+
44+
get perRequestErrors(): Readonly<Record<string, JsonRpcBatchRequestError>> {
45+
return this._perRequestErrors;
2946
}
3047

3148
protected override getCacheKey(): string {
@@ -78,15 +95,21 @@ export class ObservableJsonRpcBatchQuery<T = unknown> extends ObservableQuery<
7895
}
7996

8097
const data: Record<string, T> = {};
98+
const perRequestErrors: Record<string, JsonRpcBatchRequestError> = {};
8199

82100
for (const res of response.data) {
83101
if (res.error) {
102+
perRequestErrors[String(res.id)] = res.error;
84103
continue;
85104
}
86105

87106
data[String(res.id)] = res.result as T;
88107
}
89108

109+
runInAction(() => {
110+
this._perRequestErrors = perRequestErrors;
111+
});
112+
90113
return {
91114
headers: response.headers,
92115
data,

0 commit comments

Comments
 (0)