@toruslabs/ethereum-controllers 4.1.0

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 (88) hide show
  1. package/dist/ethereumControllers.cjs.js +6153 -0
  2. package/dist/ethereumControllers.cjs.js.map +1 -0
  3. package/dist/ethereumControllers.esm.js +5570 -0
  4. package/dist/ethereumControllers.esm.js.map +1 -0
  5. package/dist/ethereumControllers.umd.min.js +3 -0
  6. package/dist/ethereumControllers.umd.min.js.LICENSE.txt +38 -0
  7. package/dist/ethereumControllers.umd.min.js.map +1 -0
  8. package/dist/types/Account/AccountTrackerController.d.ts +35 -0
  9. package/dist/types/Block/PollingBlockTracker.d.ts +14 -0
  10. package/dist/types/Currency/CurrencyController.d.ts +30 -0
  11. package/dist/types/Gas/GasFeeController.d.ts +64 -0
  12. package/dist/types/Gas/IGasFeeController.d.ts +49 -0
  13. package/dist/types/Gas/gasUtil.d.ts +21 -0
  14. package/dist/types/Keyring/KeyringController.d.ts +20 -0
  15. package/dist/types/Message/AbstractMessageController.d.ts +36 -0
  16. package/dist/types/Message/DecryptMessageController.d.ts +20 -0
  17. package/dist/types/Message/EncryptionPublicKeyController.d.ts +20 -0
  18. package/dist/types/Message/MessageController.d.ts +20 -0
  19. package/dist/types/Message/PersonalMessageController.d.ts +20 -0
  20. package/dist/types/Message/TypedMessageController.d.ts +21 -0
  21. package/dist/types/Message/utils.d.ts +10 -0
  22. package/dist/types/Network/NetworkController.d.ts +40 -0
  23. package/dist/types/Network/createEthereumMiddleware.d.ts +66 -0
  24. package/dist/types/Network/createJsonRpcClient.d.ts +9 -0
  25. package/dist/types/Nfts/INftsController.d.ts +10 -0
  26. package/dist/types/Nfts/NftHandler.d.ts +35 -0
  27. package/dist/types/Nfts/NftsController.d.ts +40 -0
  28. package/dist/types/Preferences/PreferencesController.d.ts +53 -0
  29. package/dist/types/Tokens/ITokensController.d.ts +10 -0
  30. package/dist/types/Tokens/TokenHandler.d.ts +20 -0
  31. package/dist/types/Tokens/TokenRatesController.d.ts +42 -0
  32. package/dist/types/Tokens/TokensController.d.ts +42 -0
  33. package/dist/types/Transaction/NonceTracker.d.ts +37 -0
  34. package/dist/types/Transaction/PendingTransactionTracker.d.ts +32 -0
  35. package/dist/types/Transaction/TransactionController.d.ts +67 -0
  36. package/dist/types/Transaction/TransactionGasUtil.d.ts +21 -0
  37. package/dist/types/Transaction/TransactionStateHistoryHelper.d.ts +16 -0
  38. package/dist/types/Transaction/TransactionStateManager.d.ts +30 -0
  39. package/dist/types/Transaction/TransactionUtils.d.ts +70 -0
  40. package/dist/types/index.d.ts +43 -0
  41. package/dist/types/utils/abiDecoder.d.ts +17 -0
  42. package/dist/types/utils/abis.d.ts +84 -0
  43. package/dist/types/utils/constants.d.ts +81 -0
  44. package/dist/types/utils/contractAddresses.d.ts +1 -0
  45. package/dist/types/utils/conversionUtils.d.ts +42 -0
  46. package/dist/types/utils/helpers.d.ts +24 -0
  47. package/dist/types/utils/interfaces.d.ts +384 -0
  48. package/package.json +71 -0
  49. package/src/Account/AccountTrackerController.ts +157 -0
  50. package/src/Block/PollingBlockTracker.ts +89 -0
  51. package/src/Currency/CurrencyController.ts +117 -0
  52. package/src/Gas/GasFeeController.ts +254 -0
  53. package/src/Gas/IGasFeeController.ts +56 -0
  54. package/src/Gas/gasUtil.ts +163 -0
  55. package/src/Keyring/KeyringController.ts +118 -0
  56. package/src/Message/AbstractMessageController.ts +136 -0
  57. package/src/Message/DecryptMessageController.ts +81 -0
  58. package/src/Message/EncryptionPublicKeyController.ts +83 -0
  59. package/src/Message/MessageController.ts +74 -0
  60. package/src/Message/PersonalMessageController.ts +74 -0
  61. package/src/Message/TypedMessageController.ts +112 -0
  62. package/src/Message/utils.ts +107 -0
  63. package/src/Network/NetworkController.ts +184 -0
  64. package/src/Network/createEthereumMiddleware.ts +307 -0
  65. package/src/Network/createJsonRpcClient.ts +59 -0
  66. package/src/Nfts/INftsController.ts +13 -0
  67. package/src/Nfts/NftHandler.ts +191 -0
  68. package/src/Nfts/NftsController.ts +230 -0
  69. package/src/Preferences/PreferencesController.ts +409 -0
  70. package/src/Tokens/ITokensController.ts +13 -0
  71. package/src/Tokens/TokenHandler.ts +60 -0
  72. package/src/Tokens/TokenRatesController.ts +134 -0
  73. package/src/Tokens/TokensController.ts +278 -0
  74. package/src/Transaction/NonceTracker.ts +152 -0
  75. package/src/Transaction/PendingTransactionTracker.ts +235 -0
  76. package/src/Transaction/TransactionController.ts +558 -0
  77. package/src/Transaction/TransactionGasUtil.ts +74 -0
  78. package/src/Transaction/TransactionStateHistoryHelper.ts +41 -0
  79. package/src/Transaction/TransactionStateManager.ts +315 -0
  80. package/src/Transaction/TransactionUtils.ts +333 -0
  81. package/src/index.ts +45 -0
  82. package/src/utils/abiDecoder.ts +195 -0
  83. package/src/utils/abis.ts +677 -0
  84. package/src/utils/constants.ts +379 -0
  85. package/src/utils/contractAddresses.ts +21 -0
  86. package/src/utils/conversionUtils.ts +269 -0
  87. package/src/utils/helpers.ts +177 -0
  88. package/src/utils/interfaces.ts +454 -0
@@ -0,0 +1,230 @@
1
+ import { BaseController, PreferencesState } from "@toruslabs/base-controllers";
2
+ import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
3
+ import { BrowserProvider } from "ethers";
4
+ import { merge } from "lodash";
5
+ import log from "loglevel";
6
+
7
+ import NetworkController from "../Network/NetworkController";
8
+ import PreferencesController from "../Preferences/PreferencesController";
9
+ import { SIMPLEHASH_SUPPORTED_CHAINS } from "../utils/constants";
10
+ import { idleTimeTracker } from "../utils/helpers";
11
+ import { CustomNftInfo, CustomNftItemInfo, EthereumNetworkState, ExtendedAddressPreferences } from "../utils/interfaces";
12
+ import { NftsControllerConfig, NftsControllerState } from "./INftsController";
13
+ import { NftHandler } from "./NftHandler";
14
+
15
+ export interface INftsControllerOptions {
16
+ config?: Partial<NftsControllerConfig>;
17
+ state?: Partial<NftsControllerState>;
18
+ provider: SafeEventEmitterProvider;
19
+ getNetworkIdentifier: NetworkController["getNetworkIdentifier"];
20
+ getCustomNfts?: PreferencesController["getCustomNfts"];
21
+ getSimpleHashNfts: PreferencesController["getSimpleHashNfts"];
22
+ onPreferencesStateChange: (listener: (preferencesState: PreferencesState<ExtendedAddressPreferences>) => void) => void;
23
+ onNetworkStateChange: (listener: (networkState: EthereumNetworkState) => void) => void;
24
+ }
25
+
26
+ const DEFAULT_INTERVAL = 180 * 1000;
27
+
28
+ export class NftsController extends BaseController<NftsControllerConfig, NftsControllerState> {
29
+ name = "NftsController";
30
+
31
+ private provider: SafeEventEmitterProvider;
32
+
33
+ private ethersProvider: BrowserProvider;
34
+
35
+ private _timer: number;
36
+
37
+ private getNetworkIdentifier: NetworkController["getNetworkIdentifier"];
38
+
39
+ private getCustomNfts: PreferencesController["getCustomNfts"];
40
+
41
+ private getSimpleHashNfts: PreferencesController["getSimpleHashNfts"];
42
+
43
+ constructor({
44
+ config,
45
+ state,
46
+ provider,
47
+ getNetworkIdentifier,
48
+ getCustomNfts,
49
+ getSimpleHashNfts,
50
+ onPreferencesStateChange,
51
+ onNetworkStateChange,
52
+ }: INftsControllerOptions) {
53
+ super({ config, state });
54
+
55
+ this.provider = provider;
56
+ this.ethersProvider = new BrowserProvider(this.provider, "any");
57
+ this.getNetworkIdentifier = getNetworkIdentifier;
58
+
59
+ this.getCustomNfts = getCustomNfts;
60
+ this.getSimpleHashNfts = getSimpleHashNfts;
61
+
62
+ this.defaultConfig = {
63
+ interval: DEFAULT_INTERVAL,
64
+ selectedAddress: "",
65
+ chainId: "",
66
+ };
67
+
68
+ this.defaultState = {
69
+ nfts: {},
70
+ };
71
+ this.initialize();
72
+ onPreferencesStateChange((preferencesState) => {
73
+ if (preferencesState.selectedAddress !== this.config.selectedAddress) {
74
+ this.configure({ selectedAddress: preferencesState.selectedAddress });
75
+ this.restartNftDetection();
76
+ }
77
+ });
78
+
79
+ onNetworkStateChange((networkState) => {
80
+ const { chainId } = networkState.providerConfig;
81
+ if (chainId !== this.config.chainId) {
82
+ this.configure({ chainId });
83
+ this.restartNftDetection();
84
+ }
85
+ });
86
+ }
87
+
88
+ get userSelectedAddress(): string {
89
+ return this.config.selectedAddress;
90
+ }
91
+
92
+ get userNfts() {
93
+ if (!this.userSelectedAddress) return [];
94
+ return this.state.nfts[this.userSelectedAddress] ?? [];
95
+ }
96
+
97
+ get interval(): number {
98
+ return this.config.interval;
99
+ }
100
+
101
+ set interval(interval: number) {
102
+ if (this._timer) window.clearInterval(this._timer);
103
+ if (!interval) {
104
+ return;
105
+ }
106
+ this._timer = window.setInterval(() => {
107
+ if (!idleTimeTracker.checkIfIdle()) {
108
+ this.detectNewNfts();
109
+ this.refreshNftBalances();
110
+ }
111
+ }, interval);
112
+ }
113
+
114
+ public startNftDetection(selectedAddress: string) {
115
+ this.configure({ selectedAddress });
116
+ this.restartNftDetection();
117
+ }
118
+
119
+ /**
120
+ * Restart nft detection polling period and call detectNewNfts
121
+ * in case of address change or user session initialization.
122
+ *
123
+ */
124
+ public restartNftDetection() {
125
+ if (!this.userSelectedAddress) {
126
+ return;
127
+ }
128
+ this.detectNewNfts();
129
+ this.refreshNftBalances();
130
+ this.config.interval = DEFAULT_INTERVAL;
131
+ }
132
+
133
+ public detectNewNfts() {
134
+ const userAddress = this.userSelectedAddress;
135
+ if (!userAddress) return;
136
+ const currentChainId = this.getNetworkIdentifier();
137
+ const nftsToDetect: CustomNftInfo[] = []; // object[]
138
+ if (!currentChainId) {
139
+ this.update({ nfts: { [userAddress]: [...nftsToDetect] } });
140
+ return;
141
+ }
142
+
143
+ if (this.getCustomNfts) {
144
+ const customNfts = this.getCustomNfts(userAddress);
145
+
146
+ const reducedNfts = customNfts.reduce(
147
+ (acc, x) => {
148
+ // first aggregate by contract address
149
+ if (x.network === currentChainId) {
150
+ const newAsset: CustomNftItemInfo = {
151
+ description: "",
152
+ image: "",
153
+ name: "",
154
+ tokenBalance: "",
155
+ tokenId: x.nft_id,
156
+ customNftId: x.id.toString(),
157
+ };
158
+ if (acc[x.nft_address]) {
159
+ acc[x.nft_address].assets.push(newAsset);
160
+ } else {
161
+ const objToPush: CustomNftInfo = {
162
+ assets: [newAsset],
163
+ chainId: x.network,
164
+ contractAddress: x.nft_address,
165
+ contractName: "",
166
+ contractSymbol: "",
167
+ contractImage: "",
168
+ nftStandard: x.nft_contract_standard,
169
+ contractDescription: "",
170
+ } as CustomNftInfo;
171
+ acc[x.nft_address] = objToPush;
172
+ }
173
+ }
174
+ return acc;
175
+ },
176
+ {} as Record<string, CustomNftInfo>
177
+ );
178
+ nftsToDetect.push(...Object.values(reducedNfts));
179
+ }
180
+ this.update({ nfts: { [userAddress]: [...nftsToDetect] } });
181
+ }
182
+
183
+ async refreshNftBalances() {
184
+ const userAddress = this.userSelectedAddress;
185
+ if (userAddress === "") return;
186
+ const oldNfts = [...this.userNfts];
187
+ const nonZeroNfts: CustomNftInfo[] = [];
188
+ try {
189
+ const currentChainId = this.getNetworkIdentifier();
190
+ if (SIMPLEHASH_SUPPORTED_CHAINS.includes(currentChainId)) {
191
+ const simpleHashBalances = await this.getSimpleHashNfts(userAddress, currentChainId);
192
+ nonZeroNfts.push(...simpleHashBalances);
193
+ this.update({ nfts: { [userAddress]: nonZeroNfts } });
194
+ }
195
+ if (oldNfts.length > 0) {
196
+ this.getNftBalancesUsingHandler(oldNfts);
197
+ }
198
+ } catch (error) {
199
+ log.error(error, "unable to fetch nft balances");
200
+ }
201
+ }
202
+
203
+ async getNftBalancesUsingHandler(customNfts: CustomNftInfo[]) {
204
+ if (!this.userSelectedAddress) return;
205
+ const userAddress = this.userSelectedAddress;
206
+ const currentNetworkNfts = customNfts;
207
+ const promiseSettledResult = await Promise.allSettled(
208
+ currentNetworkNfts.map(async (x) => {
209
+ try {
210
+ const tokenInstance = new NftHandler({
211
+ ...x,
212
+ provider: this.ethersProvider,
213
+ });
214
+ const contractData = await tokenInstance.getContractMetadata();
215
+ const assetData = await Promise.allSettled(x.assets.map((y) => tokenInstance.getNftMetadata(userAddress, y)));
216
+ return {
217
+ ...contractData,
218
+ assets: assetData.filter((z) => z.status === "fulfilled").map((z) => (z as PromiseFulfilledResult<CustomNftItemInfo>).value),
219
+ } as CustomNftInfo;
220
+ } catch (error) {
221
+ log.warn("Invalid contract address while fetching", error);
222
+ return undefined;
223
+ }
224
+ })
225
+ );
226
+ const nonZeroTokens = promiseSettledResult.filter((x) => x.status === "fulfilled").map((x) => (x as PromiseFulfilledResult<CustomNftInfo>).value);
227
+
228
+ this.update({ nfts: { [userAddress]: merge(this.userNfts, nonZeroTokens) } });
229
+ }
230
+ }
@@ -0,0 +1,409 @@
1
+ import {
2
+ ACTIVITY_ACTION_RECEIVE,
3
+ ACTIVITY_ACTION_SEND,
4
+ ACTIVITY_ACTION_TOPUP,
5
+ BasePreferencesController,
6
+ CustomNft,
7
+ CustomToken,
8
+ IPreferencesController,
9
+ PreferencesConfig,
10
+ PreferencesState,
11
+ TransactionStatus,
12
+ UserInfo,
13
+ } from "@toruslabs/base-controllers";
14
+ import { get, patch, post, remove } from "@toruslabs/http-helpers";
15
+ import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
16
+ import { Mutex } from "async-mutex";
17
+ import log from "loglevel";
18
+
19
+ import PollingBlockTracker from "../Block/PollingBlockTracker";
20
+ import KeyringController from "../Keyring/KeyringController";
21
+ import NetworkController from "../Network/NetworkController";
22
+ import { SUPPORTED_NETWORKS } from "../utils/constants";
23
+ import { formatDate, formatPastTx, formatTime, getEthTxStatus } from "../utils/helpers";
24
+ import type {
25
+ CustomNetworkPayload,
26
+ CustomNetworks,
27
+ CustomNftInfo,
28
+ CustomTokenInfo,
29
+ EthereumUser,
30
+ ExtendedAddressPreferences,
31
+ FetchCommonTransaction,
32
+ FetchedTransaction,
33
+ FormattedTransactionActivity,
34
+ TransactionPayload,
35
+ } from "../utils/interfaces";
36
+
37
+ interface IPreferencesControllerOptions {
38
+ config?: Partial<PreferencesConfig> & Pick<PreferencesConfig, "api" | "commonApiHost" | "signInPrefix">;
39
+ state?: Partial<PreferencesState<ExtendedAddressPreferences>>;
40
+ provider: SafeEventEmitterProvider;
41
+ // TODO: Require later
42
+ blockTracker?: PollingBlockTracker;
43
+ signAuthMessage?: KeyringController["signAuthMessage"];
44
+ getProviderConfig?: NetworkController["getProviderConfig"];
45
+ }
46
+
47
+ export default class PreferencesController
48
+ extends BasePreferencesController<ExtendedAddressPreferences, PreferencesConfig, PreferencesState<ExtendedAddressPreferences>>
49
+ implements IPreferencesController<ExtendedAddressPreferences, PreferencesConfig, PreferencesState<ExtendedAddressPreferences>>
50
+ {
51
+ private _handle?: number;
52
+
53
+ private _mutex: Mutex = new Mutex();
54
+
55
+ private getProviderConfig: NetworkController["getProviderConfig"];
56
+
57
+ private provider: SafeEventEmitterProvider;
58
+
59
+ private blockTracker: PollingBlockTracker;
60
+
61
+ constructor({ config, state, provider, blockTracker, signAuthMessage, getProviderConfig }: IPreferencesControllerOptions) {
62
+ super({ config, state, defaultPreferences: { formattedPastTransactions: [], fetchedPastTx: [], paymentTx: [] }, signAuthMessage });
63
+ this.provider = provider;
64
+ this.getProviderConfig = getProviderConfig;
65
+ this.blockTracker = blockTracker;
66
+ log.info(this.blockTracker);
67
+ }
68
+
69
+ public async poll(interval?: number): Promise<void> {
70
+ const releaseLock = await this._mutex.acquire();
71
+ if (interval) this.configure({ pollInterval: interval });
72
+ if (this._handle) window.clearTimeout(this._handle);
73
+ // call here
74
+ const storeSelectedAddress = this.state.selectedAddress;
75
+ if (!storeSelectedAddress) return;
76
+ if (!this.getAddressState(storeSelectedAddress)?.jwtToken) return;
77
+ // This should never throw
78
+ await this.sync(storeSelectedAddress);
79
+ releaseLock();
80
+ this._handle = window.setTimeout(() => {
81
+ this.poll(this.config.pollInterval);
82
+ }, this.config.pollInterval);
83
+ }
84
+
85
+ public async initPreferences(params: {
86
+ address: string;
87
+ jwtToken?: string;
88
+ calledFromEmbed?: boolean;
89
+ userInfo?: UserInfo;
90
+ rehydrate?: boolean;
91
+ locale?: string;
92
+ }): Promise<void> {
93
+ const { address, jwtToken, calledFromEmbed, userInfo, rehydrate, locale = "en-US" } = params;
94
+ await super.init(address, userInfo, jwtToken);
95
+ const { aggregateVerifier, verifier, verifierId } = userInfo || {};
96
+ const userExists = await this.sync(address);
97
+ if (!userExists) {
98
+ const accountState = this.getAddressState(address);
99
+ await this.createUser({
100
+ selectedCurrency: accountState.selectedCurrency,
101
+ theme: accountState.theme,
102
+ verifier: aggregateVerifier || verifier,
103
+ verifierId,
104
+ locale,
105
+ address,
106
+ });
107
+ }
108
+ if (!rehydrate)
109
+ await this.storeUserLogin({ verifier: aggregateVerifier || verifier, verifierId, options: { calledFromEmbed, rehydrate }, address });
110
+ }
111
+
112
+ public getSelectedAddress(): string {
113
+ return this.state.selectedAddress;
114
+ }
115
+
116
+ async sync(address: string): Promise<boolean> {
117
+ try {
118
+ const user = await this.getUser<EthereumUser>(address);
119
+ if (user) {
120
+ const {
121
+ default_currency: defaultCurrency,
122
+ contacts,
123
+ theme,
124
+ locale,
125
+ public_address: userPublicAddress,
126
+ default_public_address: defaultPublicAddress,
127
+ customNetworks,
128
+ customTokens,
129
+ customNfts,
130
+ } = user || {};
131
+
132
+ // update latest data in state.
133
+ this.updateState(
134
+ {
135
+ contacts,
136
+ theme,
137
+ selectedCurrency: defaultCurrency,
138
+ locale,
139
+ defaultPublicAddress: defaultPublicAddress || userPublicAddress,
140
+ customTokens,
141
+ customNfts,
142
+ customNetworks,
143
+ },
144
+ address
145
+ );
146
+ return true;
147
+ }
148
+ return false;
149
+ } catch (error) {
150
+ log.error(error);
151
+ return false;
152
+ } finally {
153
+ Promise.all([
154
+ this.getWalletOrders<FetchedTransaction>(address).catch((error) => {
155
+ log.error("unable to fetch wallet orders", error);
156
+ }),
157
+ this.getTopUpOrders<FetchCommonTransaction>(address).catch((error) => {
158
+ log.error("unable to fetch top up orders", error);
159
+ }),
160
+ ])
161
+ .then((data) => {
162
+ const [walletTx, paymentTx] = data;
163
+ if (paymentTx) {
164
+ this.calculatePaymentTx(paymentTx, address);
165
+ }
166
+ // eslint-disable-next-line promise/always-return
167
+ if (walletTx && walletTx.length > 0) {
168
+ this.updateState({ fetchedPastTx: [...walletTx] }, address);
169
+ this.calculatePastTx(walletTx, address);
170
+ }
171
+ })
172
+ .catch((error) => log.error(error));
173
+ }
174
+ }
175
+
176
+ public async patchNewTx(tx: TransactionPayload, address: string): Promise<void> {
177
+ const formattedTx = formatPastTx(tx);
178
+ const storePastTx = this.getAddressState(address)?.formattedPastTransactions;
179
+ const duplicateIndex = storePastTx.findIndex((x) => x.transaction_hash === tx.transaction_hash && x.networkType === tx.network);
180
+ if (tx.status === TransactionStatus.submitted || tx.status === TransactionStatus.confirmed) {
181
+ if (duplicateIndex === -1) {
182
+ // No duplicate found
183
+
184
+ const finalTx = this.cancelTxCalculate([...storePastTx, formattedTx]);
185
+ tx.is_cancel = formattedTx.is_cancel;
186
+ tx.to = tx.to.toLowerCase();
187
+ tx.from = tx.from.toLowerCase();
188
+
189
+ this.updateState({ formattedPastTransactions: finalTx }, address);
190
+ this.postPastTx<TransactionPayload>(tx, address);
191
+ } else {
192
+ // avoid overriding is_cancel
193
+ formattedTx.is_cancel = storePastTx[duplicateIndex].is_cancel;
194
+ storePastTx[duplicateIndex] = formattedTx;
195
+ this.updateState({ formattedPastTransactions: this.cancelTxCalculate([...storePastTx]) }, address);
196
+ }
197
+ }
198
+ }
199
+
200
+ public recalculatePastTx(address?: string) {
201
+ // This triggers store update which calculates past Tx status for that network
202
+ const selectedAddress = address || this.state.selectedAddress;
203
+ const state = this.getAddressState(selectedAddress);
204
+ if (!state?.fetchedPastTx) return;
205
+ this.calculatePastTx(state.fetchedPastTx, selectedAddress);
206
+ }
207
+
208
+ public async refetchEtherscanTx(address?: string) {
209
+ const selectedAddress = address || this.state.selectedAddress;
210
+ if (this.getAddressState(selectedAddress)?.jwtToken) {
211
+ // const selectedNetwork = this.getProviderConfig().rpcTarget;
212
+ // if (ETHERSCAN_SUPPORTED_NETWORKS.has(selectedNetwork)) {
213
+ // const data = await this.fetchEtherscanTx({ selectedAddress, selectedNetwork });
214
+ // if (data.length) {
215
+ // this.emit("addEtherscanTransactions", data, selectedNetwork);
216
+ // }
217
+ // }
218
+ }
219
+ }
220
+
221
+ public async getEtherScanTokens(address: string, chainId: string): Promise<CustomTokenInfo[]> {
222
+ const selectedAddress = address;
223
+ const apiUrl = new URL(this.config.api);
224
+ apiUrl.pathname = `/tokens`;
225
+ apiUrl.searchParams.append("chainId", chainId);
226
+ apiUrl.searchParams.append("address", selectedAddress);
227
+ const result = await get<{ data: CustomTokenInfo[] }>(apiUrl.href, this.headers(this.state.selectedAddress));
228
+ return result.data;
229
+ }
230
+
231
+ public async getSimpleHashNfts(address: string, chainId: string): Promise<CustomNftInfo[]> {
232
+ const selectedAddress = address;
233
+ const apiUrl = new URL(this.config.api);
234
+ apiUrl.pathname = `/nfts`;
235
+ apiUrl.searchParams.append("chainId", chainId);
236
+ apiUrl.searchParams.append("address", selectedAddress);
237
+ const result = await get<{ data: CustomNftInfo[] }>(apiUrl.href, this.headers(this.state.selectedAddress));
238
+ return result.data;
239
+ }
240
+
241
+ public getCustomTokens(address?: string): CustomToken[] {
242
+ return this.getAddressState(address)?.customTokens ?? [];
243
+ }
244
+
245
+ public getCustomNfts(address?: string): CustomNft[] {
246
+ return this.getAddressState(address)?.customNfts ?? [];
247
+ }
248
+
249
+ // Custom Network methods
250
+ public async addCustomNetwork({ type, network }: { type: string; network: CustomNetworkPayload }): Promise<number> {
251
+ try {
252
+ const apiUrl = new URL(this.config.api);
253
+ apiUrl.pathname = `/customnetwork/${type}`;
254
+ const { selectedAddress } = this.state;
255
+ const payload = {
256
+ network_name: network.displayName,
257
+ rpc_url: network.rpcTarget,
258
+ chain_id: network.chainId,
259
+ symbol: network.ticker,
260
+ block_explorer_url: network.blockExplorerUrl || undefined,
261
+ is_test_net: network.isTestNet || false,
262
+ };
263
+ const res = await post<{ data: CustomNetworks }>(apiUrl.href, payload, this.headers(selectedAddress), { useAPIKey: true });
264
+ await this.sync(selectedAddress);
265
+
266
+ return res.data.id;
267
+ } catch {
268
+ log.error("error adding custom network");
269
+ return null;
270
+ }
271
+ }
272
+
273
+ async deleteCustomNetwork(id: number) {
274
+ try {
275
+ const { selectedAddress } = this.state;
276
+ const apiUrl = new URL(this.config.api);
277
+ apiUrl.pathname = `/customnetwork/${id}`;
278
+ await remove(apiUrl.href, {}, this.headers(selectedAddress), { useAPIKey: true });
279
+ await this.sync(selectedAddress);
280
+ return true;
281
+ } catch {
282
+ log.error("error deleting custom network");
283
+ return false;
284
+ }
285
+ }
286
+
287
+ async editCustomNetwork({ network, id }: { network: CustomNetworkPayload; id: number | null }) {
288
+ try {
289
+ const { selectedAddress } = this.state;
290
+ const apiUrl = new URL(this.config.api);
291
+ apiUrl.pathname = `/customnetwork/${id}`;
292
+
293
+ const payload = {
294
+ network_name: network.displayName,
295
+ rpc_url: network.rpcTarget,
296
+ chain_id: network.chainId,
297
+ symbol: network.ticker || undefined,
298
+ block_explorer_url: network.blockExplorerUrl || undefined,
299
+ is_test_net: network.isTestNet || false,
300
+ };
301
+ await patch(apiUrl.href, payload, this.headers(selectedAddress), { useAPIKey: true });
302
+ await this.sync(selectedAddress);
303
+ return true;
304
+ } catch {
305
+ log.error("error editing custom network");
306
+ return false;
307
+ }
308
+ }
309
+
310
+ private calculatePaymentTx(txs: FetchCommonTransaction[], address: string) {
311
+ const accumulator: FetchCommonTransaction[] = [];
312
+ for (const x of txs) {
313
+ let action = "";
314
+ const lowerCaseAction = x.action.toLowerCase();
315
+ if (ACTIVITY_ACTION_TOPUP.includes(lowerCaseAction)) action = ACTIVITY_ACTION_TOPUP;
316
+ else if (ACTIVITY_ACTION_SEND.includes(lowerCaseAction)) action = ACTIVITY_ACTION_SEND;
317
+ else if (ACTIVITY_ACTION_RECEIVE.includes(lowerCaseAction)) action = ACTIVITY_ACTION_RECEIVE;
318
+
319
+ accumulator.push({
320
+ id: x.id,
321
+ date: new Date(x.date).toDateString(),
322
+ from: x.from,
323
+ slicedFrom: x.slicedFrom,
324
+ action,
325
+ to: x.to,
326
+ slicedTo: x.slicedTo,
327
+ totalAmount: x.totalAmount,
328
+ totalAmountString: x.totalAmountString,
329
+ currencyAmount: x.currencyAmount,
330
+ currencyAmountString: x.currencyAmountString,
331
+ amount: x.amount,
332
+ ethRate: x.ethRate,
333
+ status: x.status.toLowerCase(),
334
+ etherscanLink: x.etherscanLink || "",
335
+ blockExplorerLink: "",
336
+ currencyUsed: x.currencyUsed,
337
+ });
338
+ }
339
+ this.updateState({ paymentTx: accumulator }, address);
340
+ }
341
+
342
+ private async calculatePastTx(txs: FetchedTransaction[], address: string) {
343
+ const pastTx = [];
344
+ const pendingTx = [];
345
+ const lowerCaseSelectedAddress = address.toLowerCase();
346
+ for (const x of txs) {
347
+ if (
348
+ x.network === SUPPORTED_NETWORKS[this.getProviderConfig().chainId].chainId &&
349
+ x.to &&
350
+ x.from &&
351
+ (lowerCaseSelectedAddress === x.from.toLowerCase() || lowerCaseSelectedAddress === x.to.toLowerCase())
352
+ ) {
353
+ if (x.status !== "confirmed") {
354
+ pendingTx.push(x);
355
+ } else {
356
+ const finalObject = formatPastTx(x, lowerCaseSelectedAddress);
357
+ pastTx.push(finalObject);
358
+ }
359
+ }
360
+ }
361
+ const pendingTxPromises = pendingTx.map((x) => getEthTxStatus(x.transaction_hash, this.provider).catch((error) => log.error(error)));
362
+ const resolvedTxStatuses = await Promise.all(pendingTxPromises);
363
+ for (const [index, element] of pendingTx.entries()) {
364
+ const finalObject = formatPastTx(element, lowerCaseSelectedAddress);
365
+ finalObject.status = resolvedTxStatuses[index] || TransactionStatus.submitted;
366
+ pastTx.push(finalObject);
367
+ if (lowerCaseSelectedAddress === element.from.toLowerCase() && finalObject.status && finalObject.status !== element.status)
368
+ this.patchPastTx({ id: element.id, status: finalObject.status, updated_at: new Date().toISOString() }, address);
369
+ }
370
+
371
+ const finalTx = this.cancelTxCalculate(pastTx);
372
+
373
+ this.updateState({ formattedPastTransactions: [...finalTx] }, address);
374
+ }
375
+
376
+ private cancelTxCalculate(pastTx: FormattedTransactionActivity[]) {
377
+ const nonceMap: Record<string, FormattedTransactionActivity[]> = {};
378
+ for (const x of pastTx) {
379
+ if (!nonceMap[x.nonce]) nonceMap[x.nonce] = [x];
380
+ else {
381
+ nonceMap[x.nonce].push(x);
382
+ }
383
+ }
384
+
385
+ for (const [, value] of Object.entries(nonceMap)) {
386
+ // has duplicate
387
+ if (value.length > 1) {
388
+ // get latest and mark it as is_cancel
389
+ const latestTxs = value.sort((a, b) => {
390
+ const aDate = new Date(a.date).getTime();
391
+ const bDate = new Date(b.date).getTime();
392
+ return bDate - aDate;
393
+ });
394
+ const latestCancelTx = latestTxs[0];
395
+ latestCancelTx.is_cancel = true;
396
+ latestTxs.slice(1).forEach((x) => {
397
+ x.hasCancel = true;
398
+ x.status = latestCancelTx.status === "confirmed" ? TransactionStatus.cancelled : TransactionStatus.cancelling;
399
+ x.cancelDateInitiated = `${formatTime(new Date(latestCancelTx.date).getTime())} - ${formatDate(latestCancelTx.date)}`;
400
+ x.etherscanLink = latestCancelTx.etherscanLink;
401
+ x.cancelGas = latestCancelTx.gas;
402
+ x.cancelGasPrice = latestCancelTx.gasPrice;
403
+ });
404
+ }
405
+ }
406
+
407
+ return pastTx;
408
+ }
409
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseConfig, BaseState } from "@toruslabs/base-controllers";
2
+
3
+ import { CustomTokenInfo } from "../utils/interfaces";
4
+
5
+ export interface TokensControllerConfig extends BaseConfig {
6
+ interval?: number;
7
+ selectedAddress: string;
8
+ chainId: string;
9
+ }
10
+
11
+ export interface TokensControllerState extends BaseState {
12
+ tokens: Record<string, CustomTokenInfo[]>;
13
+ }