@keplr-wallet/stores-eth 0.13.32 → 0.13.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/queries/erc20-balance-batch.d.ts +26 -0
- package/build/queries/erc20-balance-batch.js +262 -0
- package/build/queries/erc20-balance-batch.js.map +1 -0
- package/build/queries/erc20-balance-batch.spec.d.ts +1 -0
- package/build/queries/erc20-balance-batch.spec.js +157 -0
- package/build/queries/erc20-balance-batch.spec.js.map +1 -0
- package/build/queries/erc20-balance.d.ts +21 -6
- package/build/queries/erc20-balance.js +118 -27
- package/build/queries/erc20-balance.js.map +1 -1
- package/build/queries/erc20-balance.spec.d.ts +1 -0
- package/build/queries/erc20-balance.spec.js +90 -0
- package/build/queries/erc20-balance.spec.js.map +1 -0
- package/build/queries/erc20-balances.d.ts +18 -4
- package/build/queries/erc20-balances.js +200 -25
- package/build/queries/erc20-balances.js.map +1 -1
- package/build/queries/erc20-balances.spec.d.ts +1 -0
- package/build/queries/erc20-balances.spec.js +150 -0
- package/build/queries/erc20-balances.spec.js.map +1 -0
- package/build/queries/erc20-batch-parent-store.d.ts +8 -0
- package/build/queries/erc20-batch-parent-store.js +21 -0
- package/build/queries/erc20-batch-parent-store.js.map +1 -0
- package/build/queries/index.d.ts +1 -3
- package/build/queries/index.js +6 -5
- package/build/queries/index.js.map +1 -1
- package/jest.config.js +8 -1
- package/package.json +8 -8
- package/src/queries/erc20-balance-batch.spec.ts +192 -0
- package/src/queries/erc20-balance-batch.ts +273 -0
- package/src/queries/erc20-balance.spec.ts +129 -0
- package/src/queries/erc20-balance.ts +122 -36
- package/src/queries/erc20-balances.spec.ts +194 -0
- package/src/queries/erc20-balances.ts +220 -37
- package/src/queries/erc20-batch-parent-store.ts +28 -0
- package/src/queries/index.ts +8 -15
package/jest.config.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
preset: "ts-jest",
|
|
3
2
|
testEnvironment: "node",
|
|
3
|
+
watchman: false,
|
|
4
|
+
transform: {
|
|
5
|
+
"^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.check.json" }],
|
|
6
|
+
},
|
|
7
|
+
moduleNameMapper: {
|
|
8
|
+
"^@keplr-wallet/(common|crypto|mobx-utils|simple-fetch|stores|types|unit)$":
|
|
9
|
+
"<rootDir>/../$1/src",
|
|
10
|
+
},
|
|
4
11
|
testMatch: ["**/src/**/?(*.)+(spec|test).[jt]s?(x)"],
|
|
5
12
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keplr-wallet/stores-eth",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.34",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"author": "chainapsis",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"@ethersproject/strings": "^5.7.0",
|
|
28
28
|
"@ethersproject/transactions": "^5.7.0",
|
|
29
29
|
"@ethersproject/units": "^5.7.0",
|
|
30
|
-
"@keplr-wallet/common": "0.13.
|
|
31
|
-
"@keplr-wallet/simple-fetch": "0.13.
|
|
32
|
-
"@keplr-wallet/stores": "0.13.
|
|
33
|
-
"@keplr-wallet/stores-etc": "0.13.
|
|
34
|
-
"@keplr-wallet/types": "0.13.
|
|
35
|
-
"@keplr-wallet/unit": "0.13.
|
|
30
|
+
"@keplr-wallet/common": "0.13.34",
|
|
31
|
+
"@keplr-wallet/simple-fetch": "0.13.34",
|
|
32
|
+
"@keplr-wallet/stores": "0.13.34",
|
|
33
|
+
"@keplr-wallet/stores-etc": "0.13.34",
|
|
34
|
+
"@keplr-wallet/types": "0.13.34",
|
|
35
|
+
"@keplr-wallet/unit": "0.13.34",
|
|
36
36
|
"big-integer": "^1.6.48",
|
|
37
37
|
"lodash.debounce": "^4.0.8",
|
|
38
38
|
"utility-types": "^3.10.0"
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/lodash.debounce": "^4"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "261583540739c6e916c78905e587a2d82575bdc0"
|
|
48
48
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { MemoryKVStore } from "@keplr-wallet/common";
|
|
2
|
+
import { simpleFetch } from "@keplr-wallet/simple-fetch";
|
|
3
|
+
import { ChainGetter, QuerySharedContext } from "@keplr-wallet/stores";
|
|
4
|
+
import { autorun } from "mobx";
|
|
5
|
+
import { ObservableQueryEthereumERC20BalancesBatchParent } from "./erc20-balance-batch";
|
|
6
|
+
|
|
7
|
+
jest.mock("@keplr-wallet/stores", () => {
|
|
8
|
+
const context = jest.requireActual(
|
|
9
|
+
"../../../stores/src/common/query/context"
|
|
10
|
+
);
|
|
11
|
+
const batch = jest.requireActual(
|
|
12
|
+
"../../../stores/src/common/query/json-rpc-batch"
|
|
13
|
+
);
|
|
14
|
+
return {
|
|
15
|
+
QuerySharedContext: context.QuerySharedContext,
|
|
16
|
+
ObservableJsonRpcBatchQuery: batch.ObservableJsonRpcBatchQuery,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
jest.mock("@keplr-wallet/simple-fetch", () => {
|
|
21
|
+
const actual = jest.requireActual("@keplr-wallet/simple-fetch");
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
simpleFetch: jest.fn(),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const ADDRESS = "0x0000000000000000000000000000000000000001";
|
|
29
|
+
const CONTRACT_A = "0x0000000000000000000000000000000000000002";
|
|
30
|
+
const CONTRACT_B = "0x0000000000000000000000000000000000000003";
|
|
31
|
+
|
|
32
|
+
type PendingFetch = {
|
|
33
|
+
body: { id: string }[];
|
|
34
|
+
resolve: (value: { headers: Record<string, unknown>; data: unknown }) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("ObservableQueryEthereumERC20BalancesBatchParent", () => {
|
|
38
|
+
let pendingFetches: PendingFetch[];
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
pendingFetches = [];
|
|
42
|
+
(simpleFetch as jest.Mock).mockImplementation(
|
|
43
|
+
(_baseURL: string, _url: string, options: { body: string }) => {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
pendingFetches.push({
|
|
46
|
+
body: JSON.parse(options.body),
|
|
47
|
+
resolve: resolve as PendingFetch["resolve"],
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps the last known contract balance while the batch layout is rebuilt", async () => {
|
|
59
|
+
const parent = new ObservableQueryEthereumERC20BalancesBatchParent(
|
|
60
|
+
new QuerySharedContext(new MemoryKVStore("erc20-balance-batch-test"), {
|
|
61
|
+
responseDebounceMs: 0,
|
|
62
|
+
}),
|
|
63
|
+
"eip155:1",
|
|
64
|
+
mockChainGetter(),
|
|
65
|
+
ADDRESS
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
parent.addContract(CONTRACT_A);
|
|
69
|
+
const firstWait = parent.waitFreshResponse();
|
|
70
|
+
await waitForPendingFetches(pendingFetches, 1);
|
|
71
|
+
resolveFetch(pendingFetches[0], {
|
|
72
|
+
[CONTRACT_A]: "0x1",
|
|
73
|
+
});
|
|
74
|
+
await firstWait;
|
|
75
|
+
|
|
76
|
+
expect(parent.getBalance(CONTRACT_A)).toBe("0x1");
|
|
77
|
+
|
|
78
|
+
parent.addContract(CONTRACT_B);
|
|
79
|
+
const secondWait = parent.waitFreshResponse();
|
|
80
|
+
await waitForPendingFetches(pendingFetches, 2);
|
|
81
|
+
|
|
82
|
+
expect(parent.getBalance(CONTRACT_A)).toBe("0x1");
|
|
83
|
+
|
|
84
|
+
resolveFetch(pendingFetches[1], {
|
|
85
|
+
[CONTRACT_A]: "0x2",
|
|
86
|
+
[CONTRACT_B]: "0x3",
|
|
87
|
+
});
|
|
88
|
+
await secondWait;
|
|
89
|
+
|
|
90
|
+
expect(parent.getBalance(CONTRACT_A)).toBe("0x2");
|
|
91
|
+
expect(parent.getBalance(CONTRACT_B)).toBe("0x3");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("starts a rebuilt query while serving a last known balance", async () => {
|
|
95
|
+
const parent = new ObservableQueryEthereumERC20BalancesBatchParent(
|
|
96
|
+
new QuerySharedContext(new MemoryKVStore("erc20-balance-batch-test"), {
|
|
97
|
+
responseDebounceMs: 0,
|
|
98
|
+
}),
|
|
99
|
+
"eip155:1",
|
|
100
|
+
mockChainGetter(),
|
|
101
|
+
ADDRESS
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
parent.addContract(CONTRACT_A);
|
|
105
|
+
const firstWait = parent.waitFreshResponse();
|
|
106
|
+
await waitForPendingFetches(pendingFetches, 1);
|
|
107
|
+
resolveFetch(pendingFetches[0], {
|
|
108
|
+
[CONTRACT_A]: "0x1",
|
|
109
|
+
});
|
|
110
|
+
await firstWait;
|
|
111
|
+
|
|
112
|
+
parent.removeContract(CONTRACT_A);
|
|
113
|
+
await wait(250);
|
|
114
|
+
parent.addContract(CONTRACT_A);
|
|
115
|
+
await wait(250);
|
|
116
|
+
|
|
117
|
+
const disposers = [
|
|
118
|
+
autorun(() => parent.getBalance(CONTRACT_A)),
|
|
119
|
+
autorun(() => parent.getError(CONTRACT_A)),
|
|
120
|
+
autorun(() => parent.isFetchingContract(CONTRACT_A)),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
await waitForPendingFetches(pendingFetches, 2);
|
|
124
|
+
|
|
125
|
+
expect(parent.getBalance(CONTRACT_A)).toBe("0x1");
|
|
126
|
+
|
|
127
|
+
resolveFetch(pendingFetches[1], {
|
|
128
|
+
[CONTRACT_A]: "0x2",
|
|
129
|
+
});
|
|
130
|
+
await waitForCondition(() => parent.getBalance(CONTRACT_A) === "0x2");
|
|
131
|
+
|
|
132
|
+
expect(parent.getBalance(CONTRACT_A)).toBe("0x2");
|
|
133
|
+
|
|
134
|
+
disposers.forEach((dispose) => dispose());
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function mockChainGetter(): ChainGetter {
|
|
139
|
+
return {
|
|
140
|
+
getModularChain() {
|
|
141
|
+
return {
|
|
142
|
+
unwrapped: {
|
|
143
|
+
type: "evm",
|
|
144
|
+
evm: {
|
|
145
|
+
rpc: "https://rpc.example",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
} as ReturnType<ChainGetter["getModularChain"]>;
|
|
149
|
+
},
|
|
150
|
+
hasModularChain() {
|
|
151
|
+
return true;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveFetch(fetch: PendingFetch, results: Record<string, string>) {
|
|
157
|
+
fetch.resolve({
|
|
158
|
+
headers: {},
|
|
159
|
+
data: fetch.body.map((req) => ({
|
|
160
|
+
jsonrpc: "2.0",
|
|
161
|
+
id: req.id,
|
|
162
|
+
result: results[req.id] ?? "0x0",
|
|
163
|
+
})),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function waitForPendingFetches(
|
|
168
|
+
pendingFetches: PendingFetch[],
|
|
169
|
+
count: number
|
|
170
|
+
) {
|
|
171
|
+
const startedAt = Date.now();
|
|
172
|
+
while (pendingFetches.length < count) {
|
|
173
|
+
if (Date.now() - startedAt > 2000) {
|
|
174
|
+
throw new Error(`Timed out waiting for ${count} fetches`);
|
|
175
|
+
}
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function wait(ms: number) {
|
|
181
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function waitForCondition(fn: () => boolean) {
|
|
185
|
+
const startedAt = Date.now();
|
|
186
|
+
while (!fn()) {
|
|
187
|
+
if (Date.now() - startedAt > 2000) {
|
|
188
|
+
throw new Error("Timed out waiting for condition");
|
|
189
|
+
}
|
|
190
|
+
await wait(10);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChainGetter,
|
|
3
|
+
JsonRpcBatchRequest,
|
|
4
|
+
ObservableJsonRpcBatchQuery,
|
|
5
|
+
QueryError,
|
|
6
|
+
QuerySharedContext,
|
|
7
|
+
} from "@keplr-wallet/stores";
|
|
8
|
+
import { makeObservable, observable, reaction, runInAction, when } from "mobx";
|
|
9
|
+
import { erc20ContractInterface } from "../constants";
|
|
10
|
+
|
|
11
|
+
const BATCH_CHUNK_SIZE = 10;
|
|
12
|
+
const REBUILD_DEBOUNCE_MS = 200;
|
|
13
|
+
|
|
14
|
+
export class ObservableQueryEthereumERC20BalancesBatchParent {
|
|
15
|
+
@observable.shallow
|
|
16
|
+
protected refcount: Map<string, number> = new Map();
|
|
17
|
+
|
|
18
|
+
@observable.ref
|
|
19
|
+
protected batchQueries: ObservableJsonRpcBatchQuery<string>[] = [];
|
|
20
|
+
|
|
21
|
+
@observable.ref
|
|
22
|
+
protected batchQueryKeys: string[] = [];
|
|
23
|
+
|
|
24
|
+
@observable.shallow
|
|
25
|
+
protected lastKnownBalances: Map<string, string> = new Map();
|
|
26
|
+
|
|
27
|
+
@observable.ref
|
|
28
|
+
protected lastBuiltKey = "";
|
|
29
|
+
|
|
30
|
+
// Snapshot of contract → batchQueries index at rebuild time, so getError
|
|
31
|
+
// doesn't misattribute chunk ownership during debounce transitions when
|
|
32
|
+
// the live refcount no longer matches the current batchQueries layout.
|
|
33
|
+
@observable.ref
|
|
34
|
+
protected chunkIndex: Map<string, number> = new Map();
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
protected readonly sharedContext: QuerySharedContext,
|
|
38
|
+
protected readonly chainId: string,
|
|
39
|
+
protected readonly chainGetter: ChainGetter,
|
|
40
|
+
protected readonly ethereumHexAddress: string
|
|
41
|
+
) {
|
|
42
|
+
makeObservable(this);
|
|
43
|
+
|
|
44
|
+
reaction(
|
|
45
|
+
() => {
|
|
46
|
+
const keys = Array.from(this.refcount.keys()).sort().join(",");
|
|
47
|
+
// Include the RPC URL so a runtime endpoint change (user-configured
|
|
48
|
+
// custom RPC) triggers a rebuild against the new endpoint.
|
|
49
|
+
return `${this.getRpcUrl()}::${keys}`;
|
|
50
|
+
},
|
|
51
|
+
(key) => this.rebuildBatchQueries(key),
|
|
52
|
+
{ fireImmediately: true, delay: REBUILD_DEBOUNCE_MS }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
addContract(contract: string): void {
|
|
57
|
+
const key = contract.toLowerCase();
|
|
58
|
+
runInAction(() => {
|
|
59
|
+
this.refcount.set(key, (this.refcount.get(key) ?? 0) + 1);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeContract(contract: string): void {
|
|
64
|
+
const key = contract.toLowerCase();
|
|
65
|
+
const n = this.refcount.get(key);
|
|
66
|
+
if (n === undefined) return;
|
|
67
|
+
runInAction(() => {
|
|
68
|
+
if (n <= 1) {
|
|
69
|
+
this.refcount.delete(key);
|
|
70
|
+
} else {
|
|
71
|
+
this.refcount.set(key, n - 1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getBalance(contract: string): string | undefined {
|
|
77
|
+
const key = contract.toLowerCase();
|
|
78
|
+
const lastKnown = this.lastKnownBalances.get(key);
|
|
79
|
+
for (const q of this.batchQueries) {
|
|
80
|
+
trackQueryFetching(q);
|
|
81
|
+
if (!q.isStarted && lastKnown !== undefined) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = q.response?.data?.[key];
|
|
86
|
+
if (data !== undefined) return data;
|
|
87
|
+
}
|
|
88
|
+
return lastKnown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getLastKnownBalance(contract: string): string | undefined {
|
|
92
|
+
return this.lastKnownBalances.get(contract.toLowerCase());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get isFetching(): boolean {
|
|
96
|
+
return this.batchQueries.some((q) => q.isFetching);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isFetchingContract(contract: string): boolean {
|
|
100
|
+
const key = contract.toLowerCase();
|
|
101
|
+
const chunkIdx = this.chunkIndex.get(key);
|
|
102
|
+
if (chunkIdx === undefined) return false;
|
|
103
|
+
const q = this.batchQueries[chunkIdx];
|
|
104
|
+
if (!q) return false;
|
|
105
|
+
const isFetching = trackQueryFetching(q);
|
|
106
|
+
if (!q.isStarted && this.lastKnownBalances.has(key)) return false;
|
|
107
|
+
return isFetching;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Per-contract error: surface only the error of the chunk that owns this
|
|
111
|
+
// contract (plus its per-request error if any). Looks up chunk ownership
|
|
112
|
+
// from the snapshot taken at rebuild time to avoid misattribution during
|
|
113
|
+
// debounced refcount transitions.
|
|
114
|
+
getError(contract: string): QueryError<unknown> | undefined {
|
|
115
|
+
const key = contract.toLowerCase();
|
|
116
|
+
const chunkIdx = this.chunkIndex.get(key);
|
|
117
|
+
if (chunkIdx === undefined) return undefined;
|
|
118
|
+
const q = this.batchQueries[chunkIdx];
|
|
119
|
+
if (!q) return undefined;
|
|
120
|
+
trackQueryFetching(q);
|
|
121
|
+
if (!q.isStarted && this.lastKnownBalances.has(key)) return undefined;
|
|
122
|
+
if (q.error) return q.error;
|
|
123
|
+
const perReq = q.perRequestErrors[key];
|
|
124
|
+
if (perReq) {
|
|
125
|
+
return {
|
|
126
|
+
status: 0,
|
|
127
|
+
statusText: "batch-request-error",
|
|
128
|
+
message: perReq.message,
|
|
129
|
+
data: perReq as unknown,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
protected currentKey(): string {
|
|
136
|
+
const keys = Array.from(this.refcount.keys()).sort().join(",");
|
|
137
|
+
return `${this.getRpcUrl()}::${keys}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async waitFreshResponse(): Promise<void> {
|
|
141
|
+
// The reaction that rebuilds batchQueries is debounced, so refcount can
|
|
142
|
+
// change without batchQueries catching up. Wait until the built key
|
|
143
|
+
// matches the current (refcount + rpc) snapshot.
|
|
144
|
+
await when(() => this.currentKey() === this.lastBuiltKey);
|
|
145
|
+
await Promise.all(this.batchQueries.map((q) => q.waitFreshResponse()));
|
|
146
|
+
this.rememberCurrentBatchBalances();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected rebuildBatchQueries(key: string): void {
|
|
150
|
+
this.rememberCurrentBatchBalances();
|
|
151
|
+
|
|
152
|
+
if (this.refcount.size === 0) {
|
|
153
|
+
runInAction(() => {
|
|
154
|
+
this.batchQueries = [];
|
|
155
|
+
this.batchQueryKeys = [];
|
|
156
|
+
this.lastBuiltKey = key;
|
|
157
|
+
this.chunkIndex = new Map();
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const rpcUrl = this.getRpcUrl();
|
|
163
|
+
if (!rpcUrl) {
|
|
164
|
+
runInAction(() => {
|
|
165
|
+
this.batchQueries = [];
|
|
166
|
+
this.batchQueryKeys = [];
|
|
167
|
+
this.lastBuiltKey = key;
|
|
168
|
+
this.chunkIndex = new Map();
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const contracts = Array.from(this.refcount.keys()).sort();
|
|
174
|
+
const chunks = chunkArray(contracts, BATCH_CHUNK_SIZE);
|
|
175
|
+
|
|
176
|
+
const calldata = (to: string) => ({
|
|
177
|
+
to,
|
|
178
|
+
data: erc20ContractInterface.encodeFunctionData("balanceOf", [
|
|
179
|
+
this.ethereumHexAddress,
|
|
180
|
+
]),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const nextChunkIndex = new Map<string, number>();
|
|
184
|
+
chunks.forEach((chunk, idx) => {
|
|
185
|
+
for (const c of chunk) nextChunkIndex.set(c, idx);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const previousQueries = new Map<
|
|
189
|
+
string,
|
|
190
|
+
ObservableJsonRpcBatchQuery<string>
|
|
191
|
+
>();
|
|
192
|
+
this.batchQueries.forEach((q, idx) => {
|
|
193
|
+
const queryKey = this.batchQueryKeys[idx];
|
|
194
|
+
if (queryKey) {
|
|
195
|
+
previousQueries.set(queryKey, q);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const nextBatchQueryKeys = chunks.map(
|
|
200
|
+
(chunk) => `${rpcUrl}::${chunk.join(",")}`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
runInAction(() => {
|
|
204
|
+
this.batchQueries = chunks.map((chunk, idx) => {
|
|
205
|
+
const queryKey = nextBatchQueryKeys[idx];
|
|
206
|
+
const previous = previousQueries.get(queryKey);
|
|
207
|
+
if (previous) {
|
|
208
|
+
return previous;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const requests: JsonRpcBatchRequest[] = chunk.map((c) => ({
|
|
212
|
+
method: "eth_call",
|
|
213
|
+
params: [calldata(c), "latest"],
|
|
214
|
+
id: c,
|
|
215
|
+
}));
|
|
216
|
+
return new ObservableJsonRpcBatchQuery<string>(
|
|
217
|
+
this.sharedContext,
|
|
218
|
+
rpcUrl,
|
|
219
|
+
"",
|
|
220
|
+
requests
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
this.batchQueryKeys = nextBatchQueryKeys;
|
|
224
|
+
this.lastBuiltKey = key;
|
|
225
|
+
this.chunkIndex = nextChunkIndex;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
protected rememberCurrentBatchBalances(): void {
|
|
230
|
+
const entries: [string, string][] = [];
|
|
231
|
+
for (const q of this.batchQueries) {
|
|
232
|
+
const data = q.response?.data;
|
|
233
|
+
if (!data) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const [contract, balance] of Object.entries(data)) {
|
|
238
|
+
entries.push([contract.toLowerCase(), balance]);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (entries.length === 0) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
runInAction(() => {
|
|
247
|
+
for (const [contract, balance] of entries) {
|
|
248
|
+
this.lastKnownBalances.set(contract, balance);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
protected getRpcUrl(): string {
|
|
254
|
+
const u = this.chainGetter.getModularChain(this.chainId).unwrapped;
|
|
255
|
+
return u.type === "evm" || u.type === "ethermint" ? u.evm.rpc : "";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function chunkArray<T>(array: T[], size: number): T[][] {
|
|
260
|
+
const chunks: T[][] = [];
|
|
261
|
+
for (let i = 0; i < array.length; i += size) {
|
|
262
|
+
chunks.push(array.slice(i, i + size));
|
|
263
|
+
}
|
|
264
|
+
return chunks;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function trackQueryFetching(
|
|
268
|
+
query: ObservableJsonRpcBatchQuery<string>
|
|
269
|
+
): boolean {
|
|
270
|
+
// Reading `isFetching` starts ObservableQuery when a caller observes the
|
|
271
|
+
// parent state, while still letting getters return cached last-known data.
|
|
272
|
+
return query.isFetching;
|
|
273
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { MemoryKVStore } from "@keplr-wallet/common";
|
|
2
|
+
import { ChainGetter, QuerySharedContext } from "@keplr-wallet/stores";
|
|
3
|
+
import {
|
|
4
|
+
ObservableQueryEthereumERC20BalanceImpl,
|
|
5
|
+
ObservableQueryEthereumERC20BalanceRegistry,
|
|
6
|
+
} from "./erc20-balance";
|
|
7
|
+
|
|
8
|
+
jest.mock("../account", () => ({
|
|
9
|
+
EthereumAccountBase: {
|
|
10
|
+
isEthereumHexAddressWithChecksum: jest.fn(() => true),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock("@keplr-wallet/stores", () => {
|
|
15
|
+
const context = jest.requireActual(
|
|
16
|
+
"../../../stores/src/common/query/context"
|
|
17
|
+
);
|
|
18
|
+
const jsonRpc = jest.requireActual(
|
|
19
|
+
"../../../stores/src/common/query/json-rpc"
|
|
20
|
+
);
|
|
21
|
+
const map = jest.requireActual("../../../stores/src/common/map");
|
|
22
|
+
return {
|
|
23
|
+
QuerySharedContext: context.QuerySharedContext,
|
|
24
|
+
ObservableJsonRPCQuery: jsonRpc.ObservableJsonRPCQuery,
|
|
25
|
+
HasMapStore: map.HasMapStore,
|
|
26
|
+
getKeplrFromWindow: jest.fn(),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const ADDRESS = "0x0000000000000000000000000000000000000001";
|
|
31
|
+
const CONTRACT = "0x0000000000000000000000000000000000000002";
|
|
32
|
+
const DENOM = `erc20:${CONTRACT}`;
|
|
33
|
+
const CURRENCY = {
|
|
34
|
+
coinMinimalDenom: DENOM,
|
|
35
|
+
coinDenom: "TEST",
|
|
36
|
+
coinDecimals: 18,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("ObservableQueryEthereumERC20BalanceRegistry", () => {
|
|
40
|
+
it("keeps ethermint ERC20 balances on the shared batch query for hex senders", () => {
|
|
41
|
+
const batchParentStore = mockBatchParentStore();
|
|
42
|
+
const chainGetter = mockChainGetter("ethermint");
|
|
43
|
+
const registry = createRegistry(batchParentStore);
|
|
44
|
+
|
|
45
|
+
const impl = registry.getBalanceImpl(
|
|
46
|
+
"injective-1",
|
|
47
|
+
chainGetter,
|
|
48
|
+
ADDRESS,
|
|
49
|
+
DENOM
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(impl).toBeInstanceOf(ObservableQueryEthereumERC20BalanceImpl);
|
|
53
|
+
expect(batchParentStore.getOrCreate).toHaveBeenCalledWith(
|
|
54
|
+
"injective-1",
|
|
55
|
+
chainGetter,
|
|
56
|
+
ADDRESS
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps pure EVM ERC20 balances on the shared batch query", () => {
|
|
61
|
+
const batchParentStore = mockBatchParentStore();
|
|
62
|
+
const chainGetter = mockChainGetter("evm");
|
|
63
|
+
const registry = createRegistry(batchParentStore);
|
|
64
|
+
|
|
65
|
+
const impl = registry.getBalanceImpl(
|
|
66
|
+
"eip155:1",
|
|
67
|
+
chainGetter,
|
|
68
|
+
ADDRESS,
|
|
69
|
+
DENOM
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(impl).toBeInstanceOf(ObservableQueryEthereumERC20BalanceImpl);
|
|
73
|
+
expect(batchParentStore.getOrCreate).toHaveBeenCalledWith(
|
|
74
|
+
"eip155:1",
|
|
75
|
+
chainGetter,
|
|
76
|
+
ADDRESS
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function createRegistry(
|
|
82
|
+
batchParentStore: ReturnType<typeof mockBatchParentStore>
|
|
83
|
+
) {
|
|
84
|
+
return new ObservableQueryEthereumERC20BalanceRegistry(
|
|
85
|
+
new QuerySharedContext(new MemoryKVStore("erc20-balance-registry-test"), {
|
|
86
|
+
responseDebounceMs: 0,
|
|
87
|
+
}),
|
|
88
|
+
batchParentStore as any
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mockBatchParentStore() {
|
|
93
|
+
return {
|
|
94
|
+
getOrCreate: jest.fn(() => ({
|
|
95
|
+
addContract: jest.fn(),
|
|
96
|
+
removeContract: jest.fn(),
|
|
97
|
+
waitFreshResponse: jest.fn(),
|
|
98
|
+
getBalance: jest.fn(),
|
|
99
|
+
getError: jest.fn(),
|
|
100
|
+
isFetching: false,
|
|
101
|
+
})),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function mockChainGetter(type: "evm" | "ethermint"): ChainGetter {
|
|
106
|
+
return {
|
|
107
|
+
getModularChain() {
|
|
108
|
+
return {
|
|
109
|
+
type,
|
|
110
|
+
unwrapped: {
|
|
111
|
+
type,
|
|
112
|
+
evm: {
|
|
113
|
+
rpc: "https://rpc.example",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
currencies: [CURRENCY],
|
|
117
|
+
forceFindCurrency(denom: string) {
|
|
118
|
+
if (denom === DENOM) {
|
|
119
|
+
return CURRENCY;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Unknown currency: ${denom}`);
|
|
122
|
+
},
|
|
123
|
+
} as ReturnType<ChainGetter["getModularChain"]>;
|
|
124
|
+
},
|
|
125
|
+
hasModularChain() {
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|