@keplr-wallet/stores 0.13.38 → 0.13.39

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.
Files changed (33) hide show
  1. package/build/common/query/query.d.ts +1 -0
  2. package/build/common/query/query.js +23 -16
  3. package/build/common/query/query.js.map +1 -1
  4. package/build/query/cosmos/staking/delegations.d.ts +4 -0
  5. package/build/query/cosmos/staking/delegations.js +35 -3
  6. package/build/query/cosmos/staking/delegations.js.map +1 -1
  7. package/build/query/cosmos/staking/initia-delegations.d.ts +4 -0
  8. package/build/query/cosmos/staking/initia-delegations.js +42 -4
  9. package/build/query/cosmos/staking/initia-delegations.js.map +1 -1
  10. package/build/query/cosmos/staking/initia-unbonding-delegations.d.ts +4 -0
  11. package/build/query/cosmos/staking/initia-unbonding-delegations.js +31 -3
  12. package/build/query/cosmos/staking/initia-unbonding-delegations.js.map +1 -1
  13. package/build/query/cosmos/staking/response-guards.spec.d.ts +1 -0
  14. package/build/query/cosmos/staking/response-guards.spec.js +318 -0
  15. package/build/query/cosmos/staking/response-guards.spec.js.map +1 -0
  16. package/build/query/cosmos/staking/rewards.d.ts +4 -0
  17. package/build/query/cosmos/staking/rewards.js +48 -10
  18. package/build/query/cosmos/staking/rewards.js.map +1 -1
  19. package/build/query/cosmos/staking/types.d.ts +13 -2
  20. package/build/query/cosmos/staking/types.js +162 -1
  21. package/build/query/cosmos/staking/types.js.map +1 -1
  22. package/build/query/cosmos/staking/unbonding-delegations.d.ts +4 -0
  23. package/build/query/cosmos/staking/unbonding-delegations.js +30 -2
  24. package/build/query/cosmos/staking/unbonding-delegations.js.map +1 -1
  25. package/package.json +12 -11
  26. package/src/common/query/query.ts +32 -21
  27. package/src/query/cosmos/staking/delegations.ts +35 -4
  28. package/src/query/cosmos/staking/initia-delegations.ts +51 -4
  29. package/src/query/cosmos/staking/initia-unbonding-delegations.ts +30 -3
  30. package/src/query/cosmos/staking/response-guards.spec.ts +414 -0
  31. package/src/query/cosmos/staking/rewards.ts +45 -10
  32. package/src/query/cosmos/staking/types.ts +210 -4
  33. package/src/query/cosmos/staking/unbonding-delegations.ts +29 -3
@@ -0,0 +1,414 @@
1
+ import { MemoryKVStore } from "@keplr-wallet/common";
2
+ import { QuerySharedContext, QueryResponse } from "../../../common";
3
+ import { ChainGetter } from "../../../chain";
4
+ import { ObservableQueryDelegationsInner } from "./delegations";
5
+ import { ObservableQueryInitiaDelegationsInner } from "./initia-delegations";
6
+ import { ObservableQueryInitiaUnbondingDelegationsInner } from "./initia-unbonding-delegations";
7
+ import {
8
+ assertDelegationsResponse,
9
+ assertRewardsResponse,
10
+ assertStakingResponseData,
11
+ assertUnbondingDelegationsResponse,
12
+ getDelegationResponses,
13
+ getInitiaDelegationResponses,
14
+ getInitiaUnbondingResponses,
15
+ getRewardsResponse,
16
+ getUnbondingResponses,
17
+ } from "./types";
18
+
19
+ const stakeCurrency = {
20
+ coinDenom: "ATOM",
21
+ coinMinimalDenom: "uatom",
22
+ coinDecimals: 6,
23
+ };
24
+
25
+ const chainGetter: ChainGetter = {
26
+ getModularChain: () =>
27
+ ({
28
+ chainId: "cosmoshub-4",
29
+ type: "cosmos",
30
+ unwrapped: {
31
+ type: "cosmos",
32
+ cosmos: {
33
+ stakeCurrency,
34
+ },
35
+ },
36
+ } as any),
37
+ hasModularChain: () => true,
38
+ };
39
+
40
+ const initiaStakeCurrency = {
41
+ coinDenom: "INIT",
42
+ coinMinimalDenom: "uinit",
43
+ coinDecimals: 6,
44
+ };
45
+
46
+ const initiaChainGetter: ChainGetter = {
47
+ getModularChain: () =>
48
+ ({
49
+ chainId: "interwoven-1",
50
+ type: "cosmos",
51
+ unwrapped: {
52
+ type: "cosmos",
53
+ cosmos: {
54
+ stakeCurrency: initiaStakeCurrency,
55
+ },
56
+ },
57
+ } as any),
58
+ hasModularChain: () => true,
59
+ };
60
+
61
+ const initiaDelegationResponse = {
62
+ delegation_responses: [
63
+ {
64
+ delegation: {
65
+ delegator_address: "init10alvsy3f0a6vsr7ghjh3rtygrhygavsk3tscgz",
66
+ validator_address: "initvaloper1validator",
67
+ shares: [
68
+ {
69
+ denom: "uinit",
70
+ amount: "990000.000000000000000000",
71
+ },
72
+ ],
73
+ },
74
+ balance: [
75
+ {
76
+ denom: "uinit",
77
+ amount: "990000",
78
+ },
79
+ ],
80
+ },
81
+ {
82
+ delegation: {
83
+ delegator_address: "init10alvsy3f0a6vsr7ghjh3rtygrhygavsk3tscgz",
84
+ validator_address: "initvaloper1validator2",
85
+ shares: [
86
+ {
87
+ denom: "uinit",
88
+ amount: "200000.000000000000000000",
89
+ },
90
+ ],
91
+ },
92
+ balance: [
93
+ {
94
+ denom: "uinit",
95
+ amount: "200000",
96
+ },
97
+ ],
98
+ },
99
+ ],
100
+ };
101
+
102
+ const initiaUnbondingDelegationResponse = {
103
+ unbonding_responses: [
104
+ {
105
+ delegator_address: "init10alvsy3f0a6vsr7ghjh3rtygrhygavsk3tscgz",
106
+ validator_address: "initvaloper1validator",
107
+ entries: [
108
+ {
109
+ creation_height: "123",
110
+ completion_time: "2026-01-01T00:00:00Z",
111
+ initial_balance: [
112
+ {
113
+ denom: "uinit",
114
+ amount: "300000",
115
+ },
116
+ ],
117
+ balance: [
118
+ {
119
+ denom: "uinit",
120
+ amount: "200000",
121
+ },
122
+ ],
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ };
128
+
129
+ class TestDelegationsQuery extends ObservableQueryDelegationsInner {
130
+ setTestResponse(data: unknown) {
131
+ this.setResponse({
132
+ data,
133
+ staled: false,
134
+ local: true,
135
+ timestamp: Date.now(),
136
+ } as QueryResponse<any>);
137
+ }
138
+ }
139
+
140
+ class TestInitiaDelegationsQuery extends ObservableQueryInitiaDelegationsInner {
141
+ setTestResponse(data: unknown) {
142
+ this.setResponse({
143
+ data,
144
+ staled: false,
145
+ local: true,
146
+ timestamp: Date.now(),
147
+ } as QueryResponse<any>);
148
+ }
149
+ }
150
+
151
+ class TestInitiaUnbondingDelegationsQuery extends ObservableQueryInitiaUnbondingDelegationsInner {
152
+ setTestResponse(data: unknown) {
153
+ this.setResponse({
154
+ data,
155
+ staled: false,
156
+ local: true,
157
+ timestamp: Date.now(),
158
+ } as QueryResponse<any>);
159
+ }
160
+ }
161
+
162
+ describe("staking response guards", () => {
163
+ test("rejects malformed delegation responses", () => {
164
+ expect(getDelegationResponses("")).toBeUndefined();
165
+ expect(getDelegationResponses({})).toBeUndefined();
166
+ expect(
167
+ getDelegationResponses({
168
+ delegation_responses: [
169
+ {
170
+ delegation: {
171
+ delegator_address: "cosmos1delegator",
172
+ validator_address: "cosmosvaloper1validator",
173
+ shares: "1.0",
174
+ },
175
+ balance: {
176
+ denom: "uatom",
177
+ amount: "123",
178
+ },
179
+ },
180
+ ],
181
+ })
182
+ ).toHaveLength(1);
183
+
184
+ expect(() => assertDelegationsResponse("")).toThrow(
185
+ "Invalid Cosmos staking delegations response"
186
+ );
187
+ });
188
+
189
+ test("lets known malformed text responses reach the base retry path", () => {
190
+ const assertResponse = jest.fn(() => {
191
+ throw new Error("should not validate retryable string responses");
192
+ });
193
+
194
+ expect(() =>
195
+ assertStakingResponseData(
196
+ {},
197
+ "The network connection was lost.",
198
+ assertResponse
199
+ )
200
+ ).not.toThrow();
201
+ expect(assertResponse).not.toHaveBeenCalled();
202
+
203
+ expect(() =>
204
+ assertStakingResponseData(
205
+ {
206
+ get: (key: string) =>
207
+ key === "content-type" ? "application/json" : "",
208
+ },
209
+ '{"delegation_responses":[',
210
+ assertResponse
211
+ )
212
+ ).not.toThrow();
213
+ expect(assertResponse).not.toHaveBeenCalled();
214
+ });
215
+
216
+ test("still rejects unknown malformed text responses", () => {
217
+ expect(() =>
218
+ assertStakingResponseData({}, "<html>bad gateway</html>", (data) =>
219
+ assertDelegationsResponse(data)
220
+ )
221
+ ).toThrow("Invalid Cosmos staking delegations response");
222
+ });
223
+
224
+ test("rejects malformed unbonding delegation responses", () => {
225
+ expect(getUnbondingResponses("")).toBeUndefined();
226
+ expect(getUnbondingResponses({})).toBeUndefined();
227
+ expect(
228
+ getUnbondingResponses({
229
+ unbonding_responses: [
230
+ {
231
+ delegator_address: "cosmos1delegator",
232
+ validator_address: "cosmosvaloper1validator",
233
+ entries: [
234
+ {
235
+ creation_height: "1",
236
+ completion_time: "2026-01-01T00:00:00Z",
237
+ initial_balance: "123",
238
+ balance: "123",
239
+ },
240
+ ],
241
+ },
242
+ ],
243
+ })
244
+ ).toHaveLength(1);
245
+
246
+ expect(() => assertUnbondingDelegationsResponse("")).toThrow(
247
+ "Invalid Cosmos staking unbonding delegations response"
248
+ );
249
+ });
250
+
251
+ test("accepts Initia delegation responses with coin-array shares", () => {
252
+ expect(getInitiaDelegationResponses(initiaDelegationResponse)).toHaveLength(
253
+ 2
254
+ );
255
+
256
+ expect(
257
+ getInitiaDelegationResponses({
258
+ delegation_responses: [
259
+ {
260
+ delegation: {
261
+ delegator_address: "init10alvsy3f0a6vsr7ghjh3rtygrhygavsk3tscgz",
262
+ validator_address: "initvaloper1validator",
263
+ shares: [
264
+ {
265
+ denom: "uinit",
266
+ amount: 990000,
267
+ },
268
+ ],
269
+ },
270
+ balance: [
271
+ {
272
+ denom: "uinit",
273
+ amount: "990000",
274
+ },
275
+ ],
276
+ },
277
+ ],
278
+ })
279
+ ).toBeUndefined();
280
+ });
281
+
282
+ test("Initia delegation getters total coin-array balances", () => {
283
+ const query = new TestInitiaDelegationsQuery(
284
+ new QuerySharedContext(new MemoryKVStore("test"), {
285
+ responseDebounceMs: 10,
286
+ }),
287
+ "interwoven-1",
288
+ initiaChainGetter,
289
+ "init10alvsy3f0a6vsr7ghjh3rtygrhygavsk3tscgz"
290
+ );
291
+
292
+ query.setTestResponse(initiaDelegationResponse);
293
+
294
+ expect(query.total?.toCoin()).toEqual({
295
+ denom: "uinit",
296
+ amount: "1190000",
297
+ });
298
+ expect(query.delegationBalances).toHaveLength(2);
299
+ expect(query.delegations[0].delegation.shares).toBe(
300
+ "990000.000000000000000000"
301
+ );
302
+ });
303
+
304
+ test("accepts Initia unbonding responses with coin-array balances", () => {
305
+ expect(
306
+ getInitiaUnbondingResponses(initiaUnbondingDelegationResponse)
307
+ ).toHaveLength(1);
308
+
309
+ expect(
310
+ getInitiaUnbondingResponses({
311
+ unbonding_responses: [
312
+ {
313
+ delegator_address: "init10alvsy3f0a6vsr7ghjh3rtygrhygavsk3tscgz",
314
+ validator_address: "initvaloper1validator",
315
+ entries: [
316
+ {
317
+ creation_height: "123",
318
+ completion_time: "2026-01-01T00:00:00Z",
319
+ initial_balance: "300000",
320
+ balance: [
321
+ {
322
+ denom: "uinit",
323
+ amount: "200000",
324
+ },
325
+ ],
326
+ },
327
+ ],
328
+ },
329
+ ],
330
+ })
331
+ ).toBeUndefined();
332
+ });
333
+
334
+ test("Initia unbonding getters total and normalize coin-array balances", () => {
335
+ const query = new TestInitiaUnbondingDelegationsQuery(
336
+ new QuerySharedContext(new MemoryKVStore("test"), {
337
+ responseDebounceMs: 10,
338
+ }),
339
+ "interwoven-1",
340
+ initiaChainGetter,
341
+ "init10alvsy3f0a6vsr7ghjh3rtygrhygavsk3tscgz"
342
+ );
343
+
344
+ query.setTestResponse(initiaUnbondingDelegationResponse);
345
+
346
+ expect(query.total?.toCoin()).toEqual({
347
+ denom: "uinit",
348
+ amount: "200000",
349
+ });
350
+ expect(query.unbondings[0].entries[0].initial_balance).toBe("300000");
351
+ expect(query.unbondings[0].entries[0].balance).toBe("200000");
352
+ });
353
+
354
+ test("rejects malformed reward responses without coercing values", () => {
355
+ expect(getRewardsResponse("")).toBeUndefined();
356
+ expect(
357
+ getRewardsResponse({
358
+ rewards: [
359
+ {
360
+ validator_address: "cosmosvaloper1validator",
361
+ reward: [
362
+ {
363
+ denom: "uatom",
364
+ amount: "1.5",
365
+ },
366
+ ],
367
+ },
368
+ ],
369
+ total: [
370
+ {
371
+ denom: "uatom",
372
+ amount: "1.5",
373
+ },
374
+ ],
375
+ })
376
+ ).toBeDefined();
377
+ expect(
378
+ getRewardsResponse({
379
+ rewards: [
380
+ {
381
+ validator_address: "cosmosvaloper1validator",
382
+ reward: [
383
+ {
384
+ denom: "uatom",
385
+ amount: 1.5,
386
+ },
387
+ ],
388
+ },
389
+ ],
390
+ })
391
+ ).toBeUndefined();
392
+
393
+ expect(() => assertRewardsResponse("")).toThrow(
394
+ "Invalid Cosmos distribution rewards response"
395
+ );
396
+ });
397
+
398
+ test("delegation getters tolerate malformed cached responses", () => {
399
+ const query = new TestDelegationsQuery(
400
+ new QuerySharedContext(new MemoryKVStore("test"), {
401
+ responseDebounceMs: 10,
402
+ }),
403
+ "cosmoshub-4",
404
+ chainGetter,
405
+ "cosmos1delegator"
406
+ );
407
+
408
+ query.setTestResponse("");
409
+
410
+ expect(() => query.total).not.toThrow();
411
+ expect(query.delegationBalances).toEqual([]);
412
+ expect(query.delegations).toEqual([]);
413
+ });
414
+ });
@@ -1,4 +1,9 @@
1
- import { Rewards } from "./types";
1
+ import {
2
+ assertRewardsResponse,
3
+ assertStakingResponseData,
4
+ getRewardsResponse,
5
+ Rewards,
6
+ } from "./types";
2
7
  import {
3
8
  ObservableChainQuery,
4
9
  ObservableChainQueryMap,
@@ -45,16 +50,29 @@ export class ObservableQueryRewardsInner extends ObservableChainQuery<Rewards> {
45
50
  return this.bech32Address.length > 0;
46
51
  }
47
52
 
53
+ protected override async fetchResponse(abortController: AbortController) {
54
+ const response = await super.fetchResponse(abortController);
55
+ assertStakingResponseData(
56
+ response.headers,
57
+ response.data,
58
+ assertRewardsResponse
59
+ );
60
+ return response;
61
+ }
62
+
48
63
  @computed
49
64
  get rewards(): CoinPretty[] {
50
65
  const mcInfo2 = this.chainGetter.getModularChain(this.chainId);
51
66
 
52
- if (!this.response || !this.response.data.rewards) {
67
+ const response = this.response
68
+ ? getRewardsResponse(this.response.data)
69
+ : undefined;
70
+ if (!response?.rewards) {
53
71
  return [];
54
72
  }
55
73
 
56
74
  const map = new Map<string, CoinPrimitive>();
57
- for (const valRewards of this.response.data.rewards) {
75
+ for (const valRewards of response.rewards) {
58
76
  for (const coin of valRewards.reward ?? []) {
59
77
  const amount = new Dec(coin.amount).truncate();
60
78
  if (!amount.gt(new Int(0))) {
@@ -80,7 +98,10 @@ export class ObservableQueryRewardsInner extends ObservableChainQuery<Rewards> {
80
98
  (validatorAddress: string): CoinPretty[] => {
81
99
  const mcInfo2 = this.chainGetter.getModularChain(this.chainId);
82
100
 
83
- const rewards = this.response?.data.rewards?.find((r) => {
101
+ const response = this.response
102
+ ? getRewardsResponse(this.response.data)
103
+ : undefined;
104
+ const rewards = response?.rewards?.find((r) => {
84
105
  return r.validator_address === validatorAddress;
85
106
  });
86
107
 
@@ -186,7 +207,12 @@ export class ObservableQueryRewardsInner extends ObservableChainQuery<Rewards> {
186
207
 
187
208
  const result: string[] = [];
188
209
 
189
- for (const reward of this.response.data.rewards ?? []) {
210
+ const response = getRewardsResponse(this.response.data);
211
+ if (!response) {
212
+ return [];
213
+ }
214
+
215
+ for (const reward of response.rewards ?? []) {
190
216
  if (reward.reward) {
191
217
  for (const r of reward.reward) {
192
218
  const dec = new Dec(r.amount);
@@ -219,7 +245,12 @@ export class ObservableQueryRewardsInner extends ObservableChainQuery<Rewards> {
219
245
  return [];
220
246
  }
221
247
 
222
- const rewards = this.response.data.rewards?.slice() ?? [];
248
+ const response = getRewardsResponse(this.response.data);
249
+ if (!response) {
250
+ return [];
251
+ }
252
+
253
+ const rewards = response.rewards?.slice() ?? [];
223
254
  rewards.sort((reward1, reward2) => {
224
255
  const amount1 = StoreUtils.getBalanceFromCurrency(
225
256
  cosmosInfo.stakeCurrency!,
@@ -263,13 +294,17 @@ export class ObservableQueryRewardsInner extends ObservableChainQuery<Rewards> {
263
294
 
264
295
  const denoms: Set<string> = new Set();
265
296
  const mcInfo2 = this.chainGetter.getModularChain(this.chainId);
266
- if (response.data.total) {
267
- response.data.total.forEach((coin) => {
297
+ const rewardsResponse = getRewardsResponse(response.data);
298
+ if (!rewardsResponse) {
299
+ return;
300
+ }
301
+ if (rewardsResponse.total) {
302
+ rewardsResponse.total.forEach((coin) => {
268
303
  denoms.add(coin.denom);
269
304
  });
270
305
  }
271
- if (response.data.rewards) {
272
- response.data.rewards.forEach((reward) => {
306
+ if (rewardsResponse.rewards) {
307
+ rewardsResponse.rewards.forEach((reward) => {
273
308
  if (reward.reward) {
274
309
  reward.reward.forEach((r) => {
275
310
  denoms.add(r.denom);