• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

blockcoders / kuma-wallet / ebbd3c69-fda1-4bf1-80ea-77bef87d3d87

pending completion
ebbd3c69-fda1-4bf1-80ea-77bef87d3d87

Pull #8

circleci

Ruben
fix tests
Pull Request #8: Milestone 2

876 of 1103 branches covered (79.42%)

Branch coverage included in aggregate %.

3452 of 3452 new or added lines in 44 files covered. (100.0%)

6647 of 7185 relevant lines covered (92.51%)

6.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.78
/src/providers/assetProvider/AssetProvider.tsx
1
import {
2✔
2
  createContext,
2✔
3
  FC,
2✔
4
  PropsWithChildren,
2✔
5
  useContext,
2✔
6
  useEffect,
2✔
7
  useReducer,
2✔
8
  useState,
2✔
9
  useCallback,
2✔
10
} from "react";
2✔
11
import { useAccountContext, useNetworkContext } from "@src/providers";
2✔
12
import {
2✔
13
  formatAmountWithDecimals,
2✔
14
  getAssetUSDPrice,
2✔
15
  getNatitveAssetBalance,
2✔
16
} from "@src/utils/assets";
2✔
17
import { ApiPromise } from "@polkadot/api";
2✔
18
import { ethers } from "ethers";
2✔
19
import AccountEntity from "@src/storage/entities/Account";
2✔
20
import { BN, hexToBn, u8aToString } from "@polkadot/util";
2✔
21
import Extension from "@src/Extension";
2✔
22
import erc20Abi from "@src/constants/erc20.abi.json";
2✔
23
import { ContractPromise } from "@polkadot/api-contract";
2✔
24
import metadata from "@src/constants/metadata.json";
2✔
25
import { PROOF_SIZE, REF_TIME } from "@src/constants/assets";
2✔
26
import { Action, Asset, AssetContext, InitialState } from "./types";
2✔
27
import randomcolor from "randomcolor";
2✔
28
import { IAsset } from "@src/types";
2✔
29

2✔
30
export const initialState: InitialState = {
2✔
31
  assets: [],
2✔
32
  isLoadingAssets: false,
2✔
33
};
6✔
34

2✔
35
const AssetContext = createContext({} as AssetContext);
2✔
36

2✔
37
export const reducer = (state: InitialState, action: Action) => {
2✔
38
  switch (action.type) {
12✔
39
    case "loading-assets": {
12✔
40
      return {
2✔
41
        ...state,
2✔
42
        assets: [],
2✔
43
        isLoadingAssets: true,
2✔
44
      };
2✔
45
    }
2✔
46
    case "end-loading": {
12✔
47
      return {
2✔
48
        ...state,
2✔
49
        isLoadingAssets: false,
2✔
50
      };
2✔
51
    }
2✔
52
    case "set-assets": {
12✔
53
      const { assets } = action.payload;
2✔
54

2✔
55
      return {
2✔
56
        ...state,
2✔
57
        assets,
2✔
58
        isLoadingAssets: false,
2✔
59
      };
2✔
60
    }
2✔
61
    case "update-assets": {
12✔
62
      const { assets } = action.payload;
2✔
63
      return {
2✔
64
        ...state,
2✔
65
        assets,
2✔
66
      };
2✔
67
    }
2✔
68
    case "update-one-asset": {
12✔
69
      const {
4✔
70
        asset: { newValue, updatedBy, updatedByValue },
4✔
71
      } = action.payload;
4✔
72
      const assets = [...state.assets];
4✔
73

4✔
74
      const index = assets.findIndex(
4✔
75
        (asset) => asset[updatedBy] === updatedByValue
4✔
76
      );
4✔
77
      if (index > -1 && !newValue.eq(assets[index].balance)) {
4✔
78
        assets[index].balance = newValue;
2✔
79
        return {
2✔
80
          ...state,
2✔
81
          assets,
2✔
82
        };
2✔
83
      }
2✔
84
      return {
2✔
85
        ...state,
2✔
86
      };
2✔
87
    }
2✔
88
    default:
12!
89
      return state;
×
90
  }
12✔
91
};
12✔
92

2✔
93
export const AssetProvider: FC<PropsWithChildren> = ({ children }) => {
2✔
94
  const {
4✔
95
    state: { api, selectedChain, rpc, type },
4✔
96
  } = useNetworkContext();
4✔
97

4✔
98
  const {
4✔
99
    state: { selectedAccount },
4✔
100
  } = useAccountContext();
4✔
101

4✔
102
  const [state, dispatch] = useReducer(reducer, initialState);
4✔
103
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4✔
104
  const [unsubscribers, setUnsubscribers] = useState<any[]>([]);
4✔
105

4✔
106
  const loadAssets = async () => {
4✔
107
    dispatch({
4✔
108
      type: "loading-assets",
4✔
109
    });
4✔
110

4✔
111
    let assets: Asset[] = [];
4✔
112
    try {
4✔
113
      const _nativeBalance = await getNativeAsset(api, selectedAccount);
4✔
114
      const _assets = await getOtherAssets();
4✔
115
      assets = [
4✔
116
        {
4✔
117
          id: "-1",
4✔
118
          ...selectedChain?.nativeCurrency,
4✔
119
          balance: _nativeBalance,
4✔
120
        },
4✔
121
        ..._assets.map((asset) => ({
4✔
122
          ...asset,
8✔
123
          // TODO: save this colors in storage
8✔
124
          color: randomcolor(),
8✔
125
        })),
4✔
126
      ];
4✔
127
      dispatch({
4✔
128
        type: "set-assets",
4✔
129
        payload: {
4✔
130
          assets,
4✔
131
        },
4✔
132
      });
4✔
133

4✔
134
      return assets;
4✔
135
    } catch (error) {
4!
136
      dispatch({
×
137
        type: "end-loading",
×
138
      });
×
139
    } finally {
4✔
140
      getAssetsUSDPrice(assets);
4✔
141
    }
4✔
142
  };
4✔
143

4✔
144
  const getNativeAsset = async (
4✔
145
    api: ApiPromise | ethers.providers.JsonRpcProvider | null,
4✔
146
    account: AccountEntity
4✔
147
  ) => {
4✔
148
    const nativeAsset = await getNatitveAssetBalance(
4✔
149
      api,
4✔
150
      account.value.address
4✔
151
    );
4✔
152

4✔
153
    // suscribers for native asset balance update
4✔
154
    if (type === "WASM") {
4✔
155
      const unsub = await (api as ApiPromise).query.system.account(
4✔
156
        selectedAccount.value.address,
4✔
157
        ({ data }: { data: { free: string } }) => {
4✔
158
          dispatch({
4✔
159
            type: "update-one-asset",
4✔
160
            payload: {
4✔
161
              asset: {
4✔
162
                updatedBy: "id",
4✔
163
                updatedByValue: "-1",
4✔
164
                newValue: new BN(String(data?.free) || 0),
4!
165
              },
4✔
166
            },
4✔
167
          });
4✔
168
        }
4✔
169
      );
4✔
170
      // susbcribrer addde
4✔
171
      setUnsubscribers((state) => [...state, unsub]);
4✔
172
    } else {
4!
173
      const _api = api as ethers.providers.JsonRpcProvider;
×
174

×
175
      _api.removeAllListeners("block");
×
176

×
177
      _api.on("block", () => {
×
178
        _api.getBalance(account.value.address).then((balance) => {
×
179
          dispatch({
×
180
            type: "update-one-asset",
×
181
            payload: {
×
182
              asset: {
×
183
                updatedBy: "id",
×
184
                updatedByValue: "-1",
×
185
                newValue: balance,
×
186
              },
×
187
            },
×
188
          });
×
189
        });
×
190
      });
×
191
    }
×
192

4✔
193
    return nativeAsset;
4✔
194
  };
4✔
195

4✔
196
  const getOtherAssets = async () => {
4✔
197
    if (!type) return [];
4!
198

4✔
199
    if (type === "WASM") {
4✔
200
      const [assets, assetsFromPallet] = await Promise.all([
4✔
201
        loadAssetsFromStorage(),
4✔
202
        loadPolkadotAssets(),
4✔
203
      ]);
4✔
204

4✔
205
      assets.length > 0 && listWasmContracts(assets);
4✔
206

4✔
207
      return [...assetsFromPallet, ...assets];
4✔
208
    } else {
4!
209
      const assets = loadAssetsFromStorage();
×
210
      return assets;
×
211
    }
×
212
  };
4✔
213

4✔
214
  const loadPolkadotAssets = async () => {
4✔
215
    const assetPallet = await (api as ApiPromise)?.query?.assets;
4✔
216

4✔
217
    if (assetPallet?.metadata) {
4✔
218
      const assetsFromPallet = await assetPallet.metadata.entries();
4✔
219

4✔
220
      const formatedAssets: Asset[] = assetsFromPallet.map(
4✔
221
        ([
4✔
222
          {
4✔
223
            args: [id],
4✔
224
          },
4✔
225
          asset,
4✔
226
        ]) => {
4✔
227
          const _asset = asset as Partial<{
4✔
228
            name: Uint8Array;
4✔
229
            symbol: Uint8Array;
4✔
230
            decimals: Uint8Array;
4✔
231
          }>;
4✔
232

4✔
233
          return {
4✔
234
            id: String(id),
4✔
235
            name: u8aToString(_asset?.name),
4✔
236
            symbol: u8aToString(_asset?.symbol),
4✔
237
            decimals: Number(_asset?.decimals),
4✔
238
            balance: new BN("0"),
4✔
239
          };
4✔
240
        }
4✔
241
      );
4✔
242

4✔
243
      await Promise.all(
4✔
244
        formatedAssets.map((r, index) =>
4✔
245
          assetPallet
4✔
246
            ?.account(r.id, selectedAccount.value.address)
4✔
247
            .then(async (asset) => {
4✔
248
              const result = asset.toJSON() as Partial<{ balance: Uint8Array }>;
4✔
249

4✔
250
              let _balance = new BN("0");
4✔
251

4✔
252
              if (result?.balance) {
4✔
253
                if (typeof result?.balance === "number") {
4!
254
                  _balance = new BN(String(result?.balance));
×
255
                }
×
256

4✔
257
                if (
4✔
258
                  typeof result.balance === "string" &&
4!
259
                  (result.balance as string).startsWith("0x")
×
260
                ) {
4!
261
                  _balance = hexToBn(result.balance);
×
262
                }
×
263
              }
4✔
264

4✔
265
              formatedAssets[index].balance = _balance;
4✔
266

4✔
267
              const unsub = await assetPallet?.account(
4✔
268
                r.id,
4✔
269
                selectedAccount.value.address,
4✔
270
                (data: {
4✔
271
                  toJSON: () => Partial<{
4✔
272
                    balance: Uint8Array;
4✔
273
                  }>;
4✔
274
                }) => {
4✔
275
                  const result = data.toJSON();
4✔
276

4✔
277
                  let _balance = new BN("0");
4✔
278

4✔
279
                  if (result?.balance) {
4✔
280
                    if (typeof result?.balance === "number") {
4!
281
                      _balance = new BN(String(result?.balance));
×
282
                    }
×
283

4✔
284
                    if (
4✔
285
                      typeof result.balance === "string" &&
4!
286
                      (result.balance as string).startsWith("0x")
×
287
                    ) {
4!
288
                      _balance = hexToBn(result.balance);
×
289
                    }
×
290
                  }
4✔
291

4✔
292
                  dispatch({
4✔
293
                    type: "update-one-asset",
4✔
294
                    payload: {
4✔
295
                      asset: {
4✔
296
                        updatedBy: "id",
4✔
297
                        updatedByValue: r.id,
4✔
298
                        newValue: _balance,
4✔
299
                      },
4✔
300
                    },
4✔
301
                  });
4✔
302
                }
4✔
303
              );
4✔
304
              // susbcribrer added
4✔
305
              setUnsubscribers((state) => [...state, unsub]);
4✔
306
            })
4✔
307
        )
4✔
308
      );
4✔
309

4✔
310
      return formatedAssets;
4✔
311
    }
4!
312

×
313
    return [];
×
314
  };
4✔
315

4✔
316
  const loadAssetsFromStorage = async () => {
4✔
317
    const assetsFromStorage = await Extension.getAssetsByChain(
4✔
318
      selectedChain.name
4✔
319
    );
4✔
320
    const assets: IAsset[] = [];
4✔
321

4✔
322
    if (assetsFromStorage.length > 0 && selectedAccount?.value?.address) {
4✔
323
      const accountAddress = selectedAccount.value.address;
4✔
324

4✔
325
      await Promise.all(
4✔
326
        assetsFromStorage.map(async (asset, index) => {
4✔
327
          assets[index] = {
4✔
328
            address: asset.address,
4✔
329
            balance: "",
4✔
330
            id: String(index),
4✔
331
            decimals: asset.decimals,
4✔
332
            symbol: asset.symbol,
4✔
333
          };
4✔
334

4✔
335
          try {
4✔
336
            if (type === "EVM") {
4!
337
              const contract = new ethers.Contract(
×
338
                asset.address,
×
339
                erc20Abi,
×
340
                api
×
341
              );
×
342
              const balance = await contract.balanceOf(accountAddress);
×
343
              assets[index].balance = balance;
×
344

×
345
              contract.removeAllListeners("Transfer");
×
346

×
347
              contract.on("Transfer", async (from, to) => {
×
348
                const selfAddress = selectedAccount?.value?.address;
×
349
                if (from === selfAddress || to === selfAddress) {
×
350
                  const balance = await contract.balanceOf(accountAddress);
×
351
                  dispatch({
×
352
                    type: "update-one-asset",
×
353
                    payload: {
×
354
                      asset: {
×
355
                        updatedBy: "id",
×
356
                        updatedByValue: assets[index].id,
×
357
                        newValue: balance,
×
358
                      },
×
359
                    },
×
360
                  });
×
361
                }
×
362
              });
×
363
            } else {
4✔
364
              const gasLimit = api.registry.createType("WeightV2", {
4✔
365
                refTime: REF_TIME,
4✔
366
                proofSize: PROOF_SIZE,
4✔
367
              });
4✔
368

4✔
369
              const contract = new ContractPromise(
4✔
370
                api,
4✔
371
                metadata,
4✔
372
                asset.address
4✔
373
              );
4✔
374

4✔
375
              const { output } = await contract.query.balanceOf(
4✔
376
                accountAddress,
4✔
377
                {
4✔
378
                  gasLimit,
4✔
379
                },
4✔
380
                accountAddress
4✔
381
              );
4✔
382
              assets[index].balance = new BN(output?.toString() || "0");
4!
383
              assets[index].contract = contract;
4✔
384
            }
4✔
385
          } catch (error) {
4!
386
            assets[index].balance = new BN("0");
×
387
          }
×
388
        })
4✔
389
      );
4✔
390
    }
4✔
391

4✔
392
    return assets;
4✔
393
  };
4✔
394

4✔
395
  const removeListeners = () => {
4✔
396
    if (unsubscribers.length > 0) {
6✔
397
      for (const unsub of unsubscribers) {
2✔
398
        unsub?.();
6!
399
      }
6✔
400
      setUnsubscribers([]);
2✔
401
    }
2✔
402
  };
6✔
403

4✔
404
  const getAssetsUSDPrice = async (assets?: Asset[]) => {
4✔
405
    try {
4✔
406
      const copyAssets = [...(assets || state.assets)];
4!
407

4✔
408
      const addresToQuery = [];
4✔
409
      for (const [index, asset] of copyAssets.entries()) {
4✔
410
        if (asset.id === "-1" && asset.name && !asset.balance.isZero?.()) {
12!
411
          addresToQuery.push({
4✔
412
            index,
4✔
413
            asset,
4✔
414
          });
4✔
415
        }
4✔
416
      }
12✔
417

4✔
418
      await Promise.all(
4✔
419
        addresToQuery.map(async ({ asset, index }) => {
4✔
420
          const query = asset.id === "-1" ? selectedChain.name : asset.name;
4!
421

4✔
422
          const price = await getAssetUSDPrice(query as string).catch(() => 0);
4✔
423

4✔
424
          const _balance = Number(
4✔
425
            formatAmountWithDecimals(Number(asset.balance), 6, asset.decimals)
4✔
426
          );
4✔
427

4✔
428
          copyAssets[index].amount = price * _balance;
4✔
429

4✔
430
          return;
4✔
431
        })
4✔
432
      );
4✔
433

4✔
434
      dispatch({
4✔
435
        type: "update-assets",
4✔
436
        payload: {
4✔
437
          assets: copyAssets,
4✔
438
        },
4✔
439
      });
4✔
440
    } catch (error) {
4!
441
      console.log("error", error);
×
442
    }
×
443
  };
4✔
444

4✔
445
  const listWasmContracts = async (assets: IAsset[]) => {
4✔
446
    const unsub = await (api as ApiPromise).rpc.chain.subscribeNewHeads(
4✔
447
      async () => {
4✔
448
        const gasLimit = api.registry.createType("WeightV2", {
4✔
449
          refTime: REF_TIME,
4✔
450
          proofSize: PROOF_SIZE,
4✔
451
        });
4✔
452

4✔
453
        for (const asset of assets) {
4✔
454
          const { output } = await (
4✔
455
            asset.contract as ContractPromise
4✔
456
          ).query.balanceOf(
4✔
457
            selectedAccount?.value?.address,
4✔
458
            {
4✔
459
              gasLimit,
4✔
460
            },
4✔
461
            selectedAccount.value?.address
4✔
462
          );
4✔
463
          const balance = new BN(output?.toString() || "0");
4!
464

4✔
465
          dispatch({
4✔
466
            type: "update-one-asset",
4✔
467
            payload: {
4✔
468
              asset: {
4✔
469
                updatedBy: "address",
4✔
470
                updatedByValue: asset.address as string,
4✔
471
                newValue: balance,
4✔
472
              },
4✔
473
            },
4✔
474
          });
4✔
475
        }
4✔
476
      }
4✔
477
    );
4✔
478

4✔
479
    setUnsubscribers((state) => [...state, unsub]);
4✔
480
  };
4✔
481

4✔
482
  useEffect(() => {
4✔
483
    dispatch({
4✔
484
      type: "loading-assets",
4✔
485
    });
4✔
486
    removeListeners();
4✔
487
  }, [rpc, api]);
4✔
488

4✔
489
  useEffect(() => {
4✔
490
    removeListeners();
2✔
491
  }, [selectedAccount?.key]);
4✔
492

4✔
493
  useEffect(() => {
4✔
494
    if (
4✔
495
      rpc &&
4✔
496
      selectedAccount?.value?.address &&
4✔
497
      selectedChain &&
4✔
498
      type &&
4✔
499
      api
4✔
500
    ) {
4✔
501
      if (selectedAccount?.type?.includes(type)) {
4✔
502
        loadAssets();
4✔
503
      }
4✔
504
    }
4✔
505
  }, [rpc, selectedAccount, type, api]);
4✔
506

4✔
507
  useEffect(() => {
4✔
508
    let interval: NodeJS.Timer;
4✔
509
    if (state.assets.length > 0) {
4✔
510
      interval = setInterval(() => {
4✔
511
        getAssetsUSDPrice(state.assets);
×
512
      }, 60000);
4✔
513
    }
4✔
514

4✔
515
    return () => clearInterval(interval);
4✔
516
  }, [state.assets]);
4✔
517

4✔
518
  return (
4✔
519
    <AssetContext.Provider
4✔
520
      value={{
4✔
521
        state,
4✔
522
        loadAssets,
4✔
523
      }}
4✔
524
    >
4✔
525
      {children}
4✔
526
    </AssetContext.Provider>
4✔
527
  );
4✔
528
};
4✔
529

2✔
530
export const useAssetContext = () => useContext(AssetContext);
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc