@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
|
@@ -2,78 +2,159 @@ import {
|
|
|
2
2
|
BalanceRegistry,
|
|
3
3
|
ChainGetter,
|
|
4
4
|
IObservableQueryBalanceImpl,
|
|
5
|
+
QueryError,
|
|
6
|
+
QueryResponse,
|
|
5
7
|
QuerySharedContext,
|
|
6
8
|
} from "@keplr-wallet/stores";
|
|
7
9
|
import { AppCurrency } from "@keplr-wallet/types";
|
|
8
10
|
import { CoinPretty, Int } from "@keplr-wallet/unit";
|
|
9
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
computed,
|
|
13
|
+
makeObservable,
|
|
14
|
+
observable,
|
|
15
|
+
onBecomeObserved,
|
|
16
|
+
onBecomeUnobserved,
|
|
17
|
+
runInAction,
|
|
18
|
+
} from "mobx";
|
|
10
19
|
import bigInteger from "big-integer";
|
|
11
|
-
import { erc20ContractInterface } from "../constants";
|
|
12
20
|
import { DenomHelper } from "@keplr-wallet/common";
|
|
13
21
|
import { EthereumAccountBase } from "../account";
|
|
14
|
-
import {
|
|
22
|
+
import { ObservableQueryEthereumERC20BalancesBatchParent } from "./erc20-balance-batch";
|
|
23
|
+
import { ERC20BalanceBatchParentStore } from "./erc20-batch-parent-store";
|
|
15
24
|
|
|
16
25
|
export class ObservableQueryEthereumERC20BalanceImpl
|
|
17
|
-
extends ObservableEvmChainJsonRpcQuery<string>
|
|
18
26
|
implements IObservableQueryBalanceImpl
|
|
19
27
|
{
|
|
28
|
+
@observable
|
|
29
|
+
protected _observedProps = 0;
|
|
30
|
+
|
|
20
31
|
constructor(
|
|
21
|
-
|
|
22
|
-
chainId: string,
|
|
23
|
-
chainGetter: ChainGetter,
|
|
32
|
+
protected readonly parent: ObservableQueryEthereumERC20BalancesBatchParent,
|
|
33
|
+
protected readonly chainId: string,
|
|
34
|
+
protected readonly chainGetter: ChainGetter,
|
|
24
35
|
protected readonly denomHelper: DenomHelper,
|
|
25
|
-
protected readonly ethereumHexAddress: string,
|
|
26
36
|
protected readonly contractAddress: string
|
|
27
37
|
) {
|
|
28
|
-
super(sharedContext, chainId, chainGetter, "eth_call", [
|
|
29
|
-
{
|
|
30
|
-
to: contractAddress,
|
|
31
|
-
data: erc20ContractInterface.encodeFunctionData("balanceOf", [
|
|
32
|
-
ethereumHexAddress,
|
|
33
|
-
]),
|
|
34
|
-
},
|
|
35
|
-
"latest",
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
38
|
makeObservable(this);
|
|
39
|
+
|
|
40
|
+
// Readiness gates (hooks-evm) can early-return on `response` before
|
|
41
|
+
// reading `balance`, so either property being observed must register
|
|
42
|
+
// the contract into the shared batch parent.
|
|
43
|
+
const attach = (prop: "balance" | "response" | "error") => {
|
|
44
|
+
onBecomeObserved(this, prop, () => {
|
|
45
|
+
runInAction(() => {
|
|
46
|
+
this._observedProps += 1;
|
|
47
|
+
});
|
|
48
|
+
if (this._observedProps === 1) {
|
|
49
|
+
this.parent.addContract(contractAddress);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
onBecomeUnobserved(this, prop, () => {
|
|
53
|
+
runInAction(() => {
|
|
54
|
+
this._observedProps -= 1;
|
|
55
|
+
});
|
|
56
|
+
if (this._observedProps === 0) {
|
|
57
|
+
this.parent.removeContract(contractAddress);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
attach("balance");
|
|
62
|
+
attach("response");
|
|
63
|
+
attach("error");
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
@computed
|
|
42
67
|
get balance(): CoinPretty {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.getModularChain(this.chainId)
|
|
47
|
-
.findCurrency(denom);
|
|
48
|
-
|
|
49
|
-
if (!currency) {
|
|
50
|
-
throw new Error(`Unknown currency: ${this.contractAddress}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!this.response || !this.response.data) {
|
|
68
|
+
const currency = this.currency;
|
|
69
|
+
const raw = this.parent.getBalance(this.contractAddress);
|
|
70
|
+
if (raw === undefined) {
|
|
54
71
|
return new CoinPretty(currency, new Int(0)).ready(false);
|
|
55
72
|
}
|
|
56
|
-
|
|
57
73
|
return new CoinPretty(
|
|
58
74
|
currency,
|
|
59
|
-
new Int(bigInteger(
|
|
75
|
+
new Int(bigInteger(raw.replace("0x", ""), 16).toString())
|
|
60
76
|
);
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
@computed
|
|
64
80
|
get currency(): AppCurrency {
|
|
65
|
-
|
|
81
|
+
return this.chainGetter
|
|
82
|
+
.getModularChain(this.chainId)
|
|
83
|
+
.forceFindCurrency(this.denomHelper.denom);
|
|
84
|
+
}
|
|
66
85
|
|
|
86
|
+
get isFetching(): boolean {
|
|
87
|
+
return this.parent.isFetching;
|
|
88
|
+
}
|
|
89
|
+
get isObserved(): boolean {
|
|
90
|
+
return this._observedProps > 0;
|
|
91
|
+
}
|
|
92
|
+
get isStarted(): boolean {
|
|
93
|
+
return this._observedProps > 0;
|
|
94
|
+
}
|
|
95
|
+
@computed
|
|
96
|
+
get error(): QueryError<unknown> | undefined {
|
|
97
|
+
return this.parent.getError(this.contractAddress);
|
|
98
|
+
}
|
|
99
|
+
@computed
|
|
100
|
+
get response(): Readonly<QueryResponse<unknown>> | undefined {
|
|
101
|
+
// hooks-evm tx flow gates readiness on truthy `bal.response`. Synthesize
|
|
102
|
+
// from the shared batch parent so the Child advertises fetch completion.
|
|
103
|
+
const raw = this.parent.getBalance(this.contractAddress);
|
|
104
|
+
if (raw === undefined) return undefined;
|
|
105
|
+
return {
|
|
106
|
+
data: raw,
|
|
107
|
+
staled: false,
|
|
108
|
+
local: false,
|
|
109
|
+
timestamp: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
protected async ensureFetched(): Promise<void> {
|
|
114
|
+
// balanceImplMap doesn't evict on currency removal.
|
|
115
|
+
if (!this.isCurrencyRegistered()) return;
|
|
116
|
+
// Force temporary registration so imperative callers (outside a reactive
|
|
117
|
+
// observer) still trigger an actual eth_call.
|
|
118
|
+
this.parent.addContract(this.contractAddress);
|
|
119
|
+
try {
|
|
120
|
+
await this.parent.waitFreshResponse();
|
|
121
|
+
} finally {
|
|
122
|
+
this.parent.removeContract(this.contractAddress);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected isCurrencyRegistered(): boolean {
|
|
127
|
+
const target = DenomHelper.normalizeDenom(this.denomHelper.denom);
|
|
67
128
|
return this.chainGetter
|
|
68
129
|
.getModularChain(this.chainId)
|
|
69
|
-
.
|
|
130
|
+
.currencies.some(
|
|
131
|
+
(c) => DenomHelper.normalizeDenom(c.coinMinimalDenom) === target
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fetch(): Promise<void> {
|
|
136
|
+
return this.ensureFetched();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async waitFreshResponse(): Promise<
|
|
140
|
+
Readonly<QueryResponse<unknown>> | undefined
|
|
141
|
+
> {
|
|
142
|
+
await this.ensureFetched();
|
|
143
|
+
return this.response;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async waitResponse(): Promise<Readonly<QueryResponse<unknown>> | undefined> {
|
|
147
|
+
return await this.waitFreshResponse();
|
|
70
148
|
}
|
|
71
149
|
}
|
|
72
150
|
|
|
73
151
|
export class ObservableQueryEthereumERC20BalanceRegistry
|
|
74
152
|
implements BalanceRegistry
|
|
75
153
|
{
|
|
76
|
-
constructor(
|
|
154
|
+
constructor(
|
|
155
|
+
protected readonly sharedContext: QuerySharedContext,
|
|
156
|
+
protected readonly batchParentStore: ERC20BalanceBatchParentStore
|
|
157
|
+
) {}
|
|
77
158
|
|
|
78
159
|
getBalanceImpl(
|
|
79
160
|
chainId: string,
|
|
@@ -93,12 +174,17 @@ export class ObservableQueryEthereumERC20BalanceRegistry
|
|
|
93
174
|
return;
|
|
94
175
|
}
|
|
95
176
|
|
|
177
|
+
const parent = this.batchParentStore.getOrCreate(
|
|
178
|
+
chainId,
|
|
179
|
+
chainGetter,
|
|
180
|
+
address
|
|
181
|
+
);
|
|
182
|
+
|
|
96
183
|
return new ObservableQueryEthereumERC20BalanceImpl(
|
|
97
|
-
|
|
184
|
+
parent,
|
|
98
185
|
chainId,
|
|
99
186
|
chainGetter,
|
|
100
187
|
denomHelper,
|
|
101
|
-
address,
|
|
102
188
|
denomHelper.contractAddress
|
|
103
189
|
);
|
|
104
190
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { DenomHelper } from "@keplr-wallet/common";
|
|
2
|
+
import { AppCurrency } from "@keplr-wallet/types";
|
|
3
|
+
import {
|
|
4
|
+
ObservableQueryThirdpartyERC20BalanceRegistry,
|
|
5
|
+
ObservableQueryThirdpartyERC20BalancesImpl,
|
|
6
|
+
} from "./erc20-balances";
|
|
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 batch = jest.requireActual(
|
|
22
|
+
"../../../stores/src/common/query/json-rpc-batch"
|
|
23
|
+
);
|
|
24
|
+
return {
|
|
25
|
+
QuerySharedContext: context.QuerySharedContext,
|
|
26
|
+
ObservableJsonRPCQuery: jsonRpc.ObservableJsonRPCQuery,
|
|
27
|
+
ObservableJsonRpcBatchQuery: batch.ObservableJsonRpcBatchQuery,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const CONTRACT = "0x0000000000000000000000000000000000000002";
|
|
32
|
+
const DENOM = `erc20:${CONTRACT}`;
|
|
33
|
+
const CURRENCY: AppCurrency = {
|
|
34
|
+
coinMinimalDenom: DENOM,
|
|
35
|
+
coinDenom: "TEST",
|
|
36
|
+
coinDecimals: 18,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("ObservableQueryThirdpartyERC20BalancesImpl", () => {
|
|
40
|
+
it("falls back to batch eth_call for a missing token when the Alchemy response is complete", async () => {
|
|
41
|
+
const { parent, batchParent } = mockParent({
|
|
42
|
+
address: "0x0000000000000000000000000000000000000001",
|
|
43
|
+
tokenBalances: [],
|
|
44
|
+
});
|
|
45
|
+
batchParent.waitFreshResponse.mockImplementation(async () => {
|
|
46
|
+
batchParent.getBalance.mockReturnValue("0x5");
|
|
47
|
+
});
|
|
48
|
+
const balance = createBalance(parent);
|
|
49
|
+
|
|
50
|
+
await balance.fetch();
|
|
51
|
+
|
|
52
|
+
expect(batchParent.addContract).toHaveBeenCalledWith(CONTRACT);
|
|
53
|
+
expect(batchParent.waitFreshResponse).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(batchParent.removeContract).toHaveBeenCalledWith(CONTRACT);
|
|
55
|
+
expect(balance.response).not.toBe(parent.response);
|
|
56
|
+
expect(balance.balance.isReady).toBe(true);
|
|
57
|
+
expect(balance.balance.toCoin().amount).toBe("5");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("uses the batch parent's cached balance when complete Alchemy response omits the token", async () => {
|
|
61
|
+
const { parent, batchParent } = mockParent({
|
|
62
|
+
address: "0x0000000000000000000000000000000000000001",
|
|
63
|
+
tokenBalances: [],
|
|
64
|
+
});
|
|
65
|
+
batchParent.getBalance.mockReturnValue("0x5");
|
|
66
|
+
const balance = createBalance(parent);
|
|
67
|
+
|
|
68
|
+
await balance.fetch();
|
|
69
|
+
|
|
70
|
+
expect(batchParent.addContract).toHaveBeenCalledWith(CONTRACT);
|
|
71
|
+
expect(balance.balance.toCoin().amount).toBe("5");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("falls back to batch eth_call for a missing token when Alchemy has more pages", async () => {
|
|
75
|
+
const { parent, batchParent } = mockParent({
|
|
76
|
+
address: "0x0000000000000000000000000000000000000001",
|
|
77
|
+
tokenBalances: [],
|
|
78
|
+
pageKey: "next-page",
|
|
79
|
+
});
|
|
80
|
+
const balance = createBalance(parent);
|
|
81
|
+
|
|
82
|
+
await balance.fetch();
|
|
83
|
+
|
|
84
|
+
expect(batchParent.addContract).toHaveBeenCalledWith(CONTRACT);
|
|
85
|
+
expect(batchParent.waitFreshResponse).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(batchParent.removeContract).toHaveBeenCalledWith(CONTRACT);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("ObservableQueryThirdpartyERC20BalanceRegistry", () => {
|
|
91
|
+
it("does not use the Keplr Alchemy proxy for Optimism", () => {
|
|
92
|
+
const batchParentStore = {
|
|
93
|
+
getOrCreate: jest.fn(),
|
|
94
|
+
};
|
|
95
|
+
const registry = new ObservableQueryThirdpartyERC20BalanceRegistry(
|
|
96
|
+
{} as any,
|
|
97
|
+
batchParentStore as any
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const impl = registry.getBalanceImpl(
|
|
101
|
+
"eip155:10",
|
|
102
|
+
mockChainGetter(),
|
|
103
|
+
"0x0000000000000000000000000000000000000001",
|
|
104
|
+
DENOM
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(impl).toBeUndefined();
|
|
108
|
+
expect(batchParentStore.getOrCreate).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function createBalance(
|
|
113
|
+
parent: ReturnType<typeof mockParent>["parent"]
|
|
114
|
+
): ObservableQueryThirdpartyERC20BalancesImpl {
|
|
115
|
+
return new ObservableQueryThirdpartyERC20BalancesImpl(
|
|
116
|
+
parent as any,
|
|
117
|
+
"eip155:1",
|
|
118
|
+
mockChainGetter(),
|
|
119
|
+
new DenomHelper(DENOM)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mockParent(data: {
|
|
124
|
+
address: string;
|
|
125
|
+
tokenBalances: {
|
|
126
|
+
contractAddress: string;
|
|
127
|
+
tokenBalance: string | null;
|
|
128
|
+
error: { code: number; message: string } | null;
|
|
129
|
+
}[];
|
|
130
|
+
pageKey?: string;
|
|
131
|
+
}) {
|
|
132
|
+
const batchParent = {
|
|
133
|
+
addContract: jest.fn(),
|
|
134
|
+
removeContract: jest.fn(),
|
|
135
|
+
waitFreshResponse: jest.fn().mockResolvedValue(undefined),
|
|
136
|
+
getBalance: jest.fn(),
|
|
137
|
+
getLastKnownBalance: jest.fn(),
|
|
138
|
+
getError: jest.fn(),
|
|
139
|
+
isFetchingContract: jest.fn(() => false),
|
|
140
|
+
isFetching: false,
|
|
141
|
+
};
|
|
142
|
+
const response = {
|
|
143
|
+
data,
|
|
144
|
+
staled: false,
|
|
145
|
+
local: false,
|
|
146
|
+
timestamp: 0,
|
|
147
|
+
};
|
|
148
|
+
const parent = {
|
|
149
|
+
response,
|
|
150
|
+
error: undefined,
|
|
151
|
+
batchParent,
|
|
152
|
+
duplicatedFetchResolver: undefined as Promise<void> | undefined,
|
|
153
|
+
fetch: jest.fn().mockResolvedValue(undefined),
|
|
154
|
+
hasAlchemyBalance(contract: string) {
|
|
155
|
+
return data.tokenBalances.some(
|
|
156
|
+
(bal) =>
|
|
157
|
+
bal.contractAddress.toLowerCase() === contract.toLowerCase() &&
|
|
158
|
+
bal.tokenBalance != null
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
getAlchemyTokenBalance(contract: string) {
|
|
162
|
+
return data.tokenBalances.find(
|
|
163
|
+
(bal) => bal.contractAddress.toLowerCase() === contract.toLowerCase()
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
resolvesAlchemyBalance(contract: string) {
|
|
167
|
+
return this.getAlchemyTokenBalance(contract)?.tokenBalance != null;
|
|
168
|
+
},
|
|
169
|
+
isFetching: false,
|
|
170
|
+
isObserved: false,
|
|
171
|
+
isStarted: false,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return { parent, batchParent };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mockChainGetter() {
|
|
178
|
+
return {
|
|
179
|
+
getModularChain() {
|
|
180
|
+
return {
|
|
181
|
+
currencies: [CURRENCY],
|
|
182
|
+
forceFindCurrency(denom: string) {
|
|
183
|
+
if (denom === DENOM) {
|
|
184
|
+
return CURRENCY;
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Unknown currency: ${denom}`);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
hasModularChain() {
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
} as any;
|
|
194
|
+
}
|