@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,60 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { BrowserProvider, Contract } from "ethers";
3
+ import log from "loglevel";
4
+
5
+ import { ecr20Abi } from "../utils/abis";
6
+
7
+ export interface ITokenOptions {
8
+ address: string;
9
+ symbol: string;
10
+ decimals: number;
11
+ name: string;
12
+ provider: BrowserProvider;
13
+ }
14
+
15
+ export class TokenHandler {
16
+ public address: string;
17
+
18
+ public symbol: string;
19
+
20
+ public decimals: number;
21
+
22
+ public name: string;
23
+
24
+ public contract: Contract;
25
+
26
+ constructor({ address, symbol, decimals, name, provider }: ITokenOptions) {
27
+ this.address = address;
28
+
29
+ this.contract = new Contract(address, ecr20Abi, provider);
30
+ this.symbol = symbol;
31
+ this.decimals = decimals;
32
+ this.name = name;
33
+ }
34
+
35
+ public async getSymbol(): Promise<string> {
36
+ if (!this.symbol || this.symbol === "ERC20") this.symbol = await this.contract.symbol();
37
+ return this.symbol;
38
+ }
39
+
40
+ public async getDecimals(): Promise<number> {
41
+ try {
42
+ if (!this.decimals) this.decimals = await this.contract.decimals();
43
+ return this.decimals;
44
+ } catch (error) {
45
+ log.warn(`Could not get decimals for token ${this.address}`, error);
46
+ return 0;
47
+ }
48
+ }
49
+
50
+ public async getName(): Promise<string> {
51
+ if (!this.name) this.name = await this.contract.name();
52
+ return this.name;
53
+ }
54
+
55
+ public async getUserBalance(userAddress: string) {
56
+ if (!this.decimals) await this.getDecimals();
57
+ const balance = await this.contract.balanceOf(userAddress);
58
+ return new BigNumber(balance).toString(16);
59
+ }
60
+ }
@@ -0,0 +1,134 @@
1
+ import { BaseConfig, BaseController, BaseState, PreferencesState } from "@toruslabs/base-controllers";
2
+ import { get } from "@toruslabs/http-helpers";
3
+ import log from "loglevel";
4
+
5
+ import { COINGECKO_PLATFORMS_CHAIN_CODE_MAP, COINGECKO_SUPPORTED_CURRENCIES } from "../utils/constants";
6
+ import { idleTimeTracker } from "../utils/helpers";
7
+ import { CustomTokenInfo, EthereumNetworkState, ExtendedAddressPreferences } from "../utils/interfaces";
8
+ import { TokensControllerState } from "./ITokensController";
9
+
10
+ export interface CoinGeckoResponse {
11
+ [address: string]: {
12
+ [currency: string]: number;
13
+ };
14
+ }
15
+
16
+ type ContractExchangeRates = Record<string, number | undefined>;
17
+
18
+ export const DEFAULT_CURRENCY = "eth";
19
+
20
+ export interface ITokenRatesControllerState extends BaseState {
21
+ contractExchangeRates: ContractExchangeRates;
22
+ }
23
+
24
+ export interface ITokenRatesControllerConfig extends BaseConfig {
25
+ pollInterval: number;
26
+ api: string;
27
+ currencyApi: string;
28
+ chainId: string;
29
+ selectedAddress: string;
30
+ nativeCurrency: string;
31
+ tokens: CustomTokenInfo[];
32
+ }
33
+
34
+ export interface TokenRatesControllerOptions {
35
+ config: Partial<ITokenRatesControllerConfig>;
36
+ state: Partial<ITokenRatesControllerState>;
37
+ onPreferencesStateChange: (listener: (preferencesState: PreferencesState<ExtendedAddressPreferences>) => void) => void;
38
+ onTokensStateChange: (listener: (tokensState: TokensControllerState) => void) => void;
39
+ onNetworkStateChange: (listener: (networkState: EthereumNetworkState) => void) => void;
40
+ }
41
+
42
+ export class TokenRatesController extends BaseController<ITokenRatesControllerConfig, ITokenRatesControllerState> {
43
+ private conversionInterval: number;
44
+
45
+ constructor({ config, state, onPreferencesStateChange, onNetworkStateChange, onTokensStateChange }: TokenRatesControllerOptions) {
46
+ super({ config, state });
47
+ this.defaultState = {
48
+ ...this.defaultState,
49
+ contractExchangeRates: {},
50
+ };
51
+ this.initialize();
52
+
53
+ onPreferencesStateChange((preferencesState) => {
54
+ const { selectedAddress } = preferencesState;
55
+ this.configure({ selectedAddress });
56
+ });
57
+
58
+ onNetworkStateChange((networkState) => {
59
+ const { chainId, ticker } = networkState.providerConfig;
60
+ this.configure({ chainId, nativeCurrency: ticker });
61
+ });
62
+
63
+ onTokensStateChange((tokensState) => {
64
+ const { tokens } = tokensState;
65
+ const currentUserTokens = tokens[this.config.selectedAddress];
66
+ if (currentUserTokens?.length > 0 && this.config.tokens !== tokens[this.config.selectedAddress]) {
67
+ this.configure({ tokens: tokens[this.config.selectedAddress] });
68
+ this.updateExchangeRates();
69
+ }
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Creates a new poll, using setInterval, to periodically call updateConversionRate. The id of the interval is
75
+ * stored at the controller's conversionInterval property. If it is called and such an id already exists, the
76
+ * previous interval is clear and a new one is created.
77
+ */
78
+ public scheduleConversionInterval(): void {
79
+ if (this.conversionInterval) {
80
+ window.clearInterval(this.conversionInterval);
81
+ }
82
+ this.conversionInterval = window.setInterval(() => {
83
+ if (!idleTimeTracker.checkIfIdle()) {
84
+ this.updateExchangeRates();
85
+ }
86
+ }, this.config.pollInterval);
87
+ }
88
+
89
+ public async updateExchangeRates() {
90
+ const chainCodes = COINGECKO_PLATFORMS_CHAIN_CODE_MAP[this.config.chainId];
91
+ let newContractExchangeRates: ContractExchangeRates = {};
92
+ if (!chainCodes) {
93
+ log.info(`ChainId ${this.config.chainId} not supported by coingecko`);
94
+ this.config.tokens.forEach((token) => {
95
+ newContractExchangeRates[token.tokenAddress] = undefined;
96
+ });
97
+ } else {
98
+ newContractExchangeRates = await this.fetchExchangeRates(this.config.nativeCurrency, chainCodes);
99
+ }
100
+ this.update({ contractExchangeRates: newContractExchangeRates });
101
+ }
102
+
103
+ private async fetchExchangeRates(nativeCurrency: string, chainCodes: { platform: string; currency: string }): Promise<ContractExchangeRates> {
104
+ const contractAddresses = this.config.tokens.map((token) => token.tokenAddress);
105
+
106
+ const isNativeCurrencySupported = COINGECKO_SUPPORTED_CURRENCIES.has(nativeCurrency.toLowerCase());
107
+ if (isNativeCurrencySupported) {
108
+ const response = await get<CoinGeckoResponse>(
109
+ `${this.config.api}/simple/token_price/${chainCodes.platform}?contract_addresses=${contractAddresses.join(
110
+ ","
111
+ )}&vs_currencies=${nativeCurrency.toLowerCase()}&include_market_cap=false&include_24hr_vol=false&include_24hr_change=false&include_last_updated_at=false`
112
+ );
113
+ const newContractExchangeRates: ContractExchangeRates = {};
114
+ Object.keys(response).forEach((contractAddress) => {
115
+ newContractExchangeRates[contractAddress] = response[contractAddress][nativeCurrency.toLowerCase()] || 0;
116
+ });
117
+ return newContractExchangeRates;
118
+ }
119
+ const [response, currencyResponse] = await Promise.all([
120
+ get<CoinGeckoResponse>(
121
+ `${this.config.api}/simple/token_price/${chainCodes.platform}?contract_addresses=${contractAddresses.join(
122
+ ","
123
+ )}&vs_currencies=${DEFAULT_CURRENCY}&include_market_cap=false&include_24hr_vol=false&include_24hr_change=false&include_last_updated_at=false`
124
+ ),
125
+ get<Record<string, string>>(`${this.config.currencyApi}/currency?fsym=${nativeCurrency.toUpperCase()}&tsyms=${DEFAULT_CURRENCY.toUpperCase()}`),
126
+ ]);
127
+ const newContractExchangeRates: ContractExchangeRates = {};
128
+ Object.keys(response).forEach((contractAddress) => {
129
+ newContractExchangeRates[contractAddress] =
130
+ response[contractAddress][DEFAULT_CURRENCY] * Number.parseFloat(currencyResponse[DEFAULT_CURRENCY]) || 0;
131
+ });
132
+ return newContractExchangeRates;
133
+ }
134
+ }
@@ -0,0 +1,278 @@
1
+ import { BaseController, PreferencesState } from "@toruslabs/base-controllers";
2
+ import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
3
+ import { BrowserProvider, Contract, toQuantity } from "ethers";
4
+ import log from "loglevel";
5
+
6
+ import NetworkController from "../Network/NetworkController";
7
+ import PreferencesController from "../Preferences/PreferencesController";
8
+ import { singleBalanceCheckerAbi } from "../utils/abis";
9
+ import { ETHERSCAN_SUPPORTED_CHAINS } from "../utils/constants";
10
+ import { SINGLE_CALL_BALANCES_ADDRESSES } from "../utils/contractAddresses";
11
+ import { idleTimeTracker, toChecksumAddressByChainId } from "../utils/helpers";
12
+ import { CustomTokenInfo, EthereumNetworkState, ExtendedAddressPreferences } from "../utils/interfaces";
13
+ import { TokensControllerConfig, TokensControllerState } from "./ITokensController";
14
+ import { TokenHandler } from "./TokenHandler";
15
+
16
+ export interface ITokensControllerOptions {
17
+ config?: Partial<TokensControllerConfig>;
18
+ state?: Partial<TokensControllerState>;
19
+ provider: SafeEventEmitterProvider;
20
+ getNetworkIdentifier: NetworkController["getNetworkIdentifier"];
21
+ getCustomTokens?: PreferencesController["getCustomTokens"];
22
+ getEtherScanTokens: PreferencesController["getEtherScanTokens"];
23
+ getProviderConfig: NetworkController["getProviderConfig"];
24
+ onPreferencesStateChange: (listener: (preferencesState: PreferencesState<ExtendedAddressPreferences>) => void) => void;
25
+ onNetworkStateChange: (listener: (networkState: EthereumNetworkState) => void) => void;
26
+ }
27
+
28
+ function getObjectFromArrayBasedonKey(oldArray: CustomTokenInfo[], key: string) {
29
+ return oldArray.reduce((acc: Record<string, CustomTokenInfo>, x) => {
30
+ const xkey = x[key as keyof CustomTokenInfo];
31
+ if (typeof xkey === "boolean") return acc;
32
+ acc[xkey] = x;
33
+ return acc;
34
+ }, {});
35
+ }
36
+
37
+ const mergeTokenArrays = (oldArray: CustomTokenInfo[], newArray: CustomTokenInfo[]): CustomTokenInfo[] => {
38
+ const oldMap = getObjectFromArrayBasedonKey(oldArray || [], "tokenAddress");
39
+ const newMap = getObjectFromArrayBasedonKey(newArray || [], "tokenAddress");
40
+ const finalArr = newArray;
41
+ Object.keys(oldMap).forEach((x) => {
42
+ if (!newMap[x] && oldMap[x].isEtherScan) finalArr.push(oldMap[x]);
43
+ });
44
+ return finalArr;
45
+ };
46
+
47
+ const DEFAULT_INTERVAL = 180 * 1000;
48
+
49
+ export class TokensController extends BaseController<TokensControllerConfig, TokensControllerState> {
50
+ name = "TokensController";
51
+
52
+ private provider: SafeEventEmitterProvider;
53
+
54
+ private ethersProvider: BrowserProvider;
55
+
56
+ private _timer: number;
57
+
58
+ private getNetworkIdentifier: NetworkController["getNetworkIdentifier"];
59
+
60
+ private getProviderConfig: NetworkController["getProviderConfig"];
61
+
62
+ private getCustomTokens: PreferencesController["getCustomTokens"];
63
+
64
+ private getEtherScanTokens: PreferencesController["getEtherScanTokens"];
65
+
66
+ constructor({
67
+ config,
68
+ state,
69
+ provider,
70
+ getNetworkIdentifier,
71
+ getCustomTokens,
72
+ getEtherScanTokens,
73
+ getProviderConfig,
74
+ onPreferencesStateChange,
75
+ onNetworkStateChange,
76
+ }: ITokensControllerOptions) {
77
+ super({ config, state });
78
+
79
+ this.provider = provider;
80
+ this.ethersProvider = new BrowserProvider(this.provider, "any");
81
+ this.getNetworkIdentifier = getNetworkIdentifier;
82
+
83
+ this.getCustomTokens = getCustomTokens;
84
+ this.getEtherScanTokens = getEtherScanTokens;
85
+ this.getProviderConfig = getProviderConfig;
86
+
87
+ this.defaultConfig = {
88
+ interval: DEFAULT_INTERVAL,
89
+ selectedAddress: "",
90
+ chainId: "",
91
+ };
92
+
93
+ this.defaultState = {
94
+ tokens: {},
95
+ };
96
+ this.initialize();
97
+
98
+ onPreferencesStateChange((preferencesState) => {
99
+ if (preferencesState.selectedAddress !== this.config.selectedAddress) {
100
+ this.configure({ selectedAddress: preferencesState.selectedAddress });
101
+ this.restartTokenDetection();
102
+ }
103
+ });
104
+
105
+ onNetworkStateChange((networkState) => {
106
+ const { chainId } = networkState.providerConfig;
107
+ if (chainId !== this.config.chainId) {
108
+ this.configure({ chainId });
109
+ this.restartTokenDetection();
110
+ }
111
+ });
112
+ }
113
+
114
+ get userSelectedAddress(): string {
115
+ return this.config.selectedAddress;
116
+ }
117
+
118
+ get userTokens() {
119
+ if (!this.userSelectedAddress) return [];
120
+ return this.state.tokens[this.userSelectedAddress] ?? [];
121
+ }
122
+
123
+ get interval(): number {
124
+ return this.config.interval;
125
+ }
126
+
127
+ set interval(interval: number) {
128
+ if (this._timer) window.clearInterval(this._timer);
129
+ if (!interval) {
130
+ return;
131
+ }
132
+ this._timer = window.setInterval(() => {
133
+ if (!idleTimeTracker.checkIfIdle()) {
134
+ this.detectNewTokens();
135
+ this.refreshTokenBalances();
136
+ }
137
+ }, interval);
138
+ }
139
+
140
+ public startTokenDetection(selectedAddress: string) {
141
+ this.configure({ selectedAddress });
142
+ this.restartTokenDetection();
143
+ }
144
+
145
+ /**
146
+ * Restart token detection polling period and call detectNewTokens
147
+ * in case of address change or user session initialization.
148
+ *
149
+ */
150
+ public restartTokenDetection() {
151
+ if (!this.userSelectedAddress) {
152
+ return;
153
+ }
154
+ this.detectNewTokens();
155
+ this.refreshTokenBalances();
156
+ this.config.interval = DEFAULT_INTERVAL;
157
+ }
158
+
159
+ public detectNewTokens() {
160
+ const userAddress = this.userSelectedAddress;
161
+ if (!userAddress) return;
162
+ const currentChainId = this.getNetworkIdentifier();
163
+ const tokens: CustomTokenInfo[] = []; // object[]
164
+ if (!currentChainId) {
165
+ this.update({ tokens: { [userAddress]: [...tokens] } });
166
+ return;
167
+ }
168
+
169
+ const networkConfig = this.getProviderConfig();
170
+
171
+ if (networkConfig?.isErc20 && networkConfig?.tokenAddress) {
172
+ tokens.push({
173
+ tokenAddress: networkConfig.tokenAddress,
174
+ name: networkConfig.tickerName,
175
+ logo: networkConfig.logo,
176
+ erc20: true,
177
+ symbol: networkConfig.ticker,
178
+ decimals: "18",
179
+ chainId: currentChainId,
180
+ });
181
+ }
182
+ if (this.getCustomTokens) {
183
+ const customTokens = this.getCustomTokens(userAddress);
184
+ tokens.push(
185
+ ...customTokens.reduce((acc, x) => {
186
+ if (x.network === currentChainId)
187
+ acc.push({
188
+ tokenAddress: x.token_address,
189
+ name: x.token_name,
190
+ logo: "eth.svg",
191
+ erc20: true,
192
+ symbol: x.token_symbol,
193
+ decimals: x.decimals,
194
+ balance: "",
195
+ customTokenId: x.id.toString(),
196
+ chainId: x.network,
197
+ });
198
+ return acc;
199
+ }, [] as CustomTokenInfo[])
200
+ );
201
+ }
202
+ this.update({ tokens: { [userAddress]: [...tokens] } });
203
+ }
204
+
205
+ async refreshTokenBalances() {
206
+ const userAddress = this.userSelectedAddress;
207
+ if (userAddress === "") return;
208
+ const oldTokens = [...this.userTokens];
209
+ const tokenAddresses = oldTokens.map((x) => x.tokenAddress);
210
+ const nonZeroTokens: CustomTokenInfo[] = [];
211
+ try {
212
+ const currentChainId = this.getNetworkIdentifier();
213
+ if (ETHERSCAN_SUPPORTED_CHAINS.includes(currentChainId)) {
214
+ const etherscanBalances = await this.getEtherScanTokens(userAddress, currentChainId);
215
+ nonZeroTokens.push(...etherscanBalances);
216
+ }
217
+ if (tokenAddresses.length > 0) {
218
+ const currentSingleCallAddress = SINGLE_CALL_BALANCES_ADDRESSES[currentChainId];
219
+ if (currentSingleCallAddress) {
220
+ const ethContract = new Contract(currentSingleCallAddress, singleBalanceCheckerAbi, this.ethersProvider);
221
+ const result = await ethContract.balances([userAddress], tokenAddresses);
222
+ tokenAddresses.forEach((_, index) => {
223
+ const balance = toQuantity(result[index]);
224
+ if (balance && balance !== "0x0") {
225
+ nonZeroTokens.push({ ...oldTokens[index], balance, chainId: currentChainId });
226
+ }
227
+ });
228
+ } else {
229
+ this.getTokenBalancesUsingHandler(oldTokens);
230
+ }
231
+ }
232
+ } catch (error) {
233
+ log.error(error, "unable to fetch token balances using single call balance address");
234
+ this.getTokenBalancesUsingHandler(oldTokens);
235
+ } finally {
236
+ this.update({ tokens: { [userAddress]: nonZeroTokens } });
237
+ }
238
+ }
239
+
240
+ async getTokenBalancesUsingHandler(customTokens: CustomTokenInfo[]) {
241
+ if (!this.userSelectedAddress) return;
242
+ const currentNetworkTokens = customTokens;
243
+ const promiseSettledResult = await Promise.allSettled(
244
+ currentNetworkTokens.map(async (x) => {
245
+ try {
246
+ const tokenInstance = new TokenHandler({
247
+ address: x.tokenAddress,
248
+ decimals: Number.parseInt(x.decimals),
249
+ name: x.name,
250
+ symbol: x.symbol,
251
+ provider: this.ethersProvider,
252
+ });
253
+ const balance = await tokenInstance.getUserBalance(this.userSelectedAddress);
254
+ return {
255
+ decimals: tokenInstance.decimals.toString(),
256
+ erc20: true,
257
+ logo: x.logo || "eth.svg",
258
+ name: tokenInstance.name,
259
+ symbol: tokenInstance.symbol,
260
+ tokenAddress: toChecksumAddressByChainId(tokenInstance.address, x.chainId),
261
+ balance: `0x${balance}`,
262
+ customTokenId: x.customTokenId,
263
+ network: x.chainId,
264
+ chainId: x.chainId,
265
+ } as CustomTokenInfo;
266
+ } catch (error) {
267
+ log.warn("Invalid contract address while fetching", error);
268
+ return undefined;
269
+ }
270
+ })
271
+ );
272
+ const nonZeroTokens = promiseSettledResult
273
+ .filter((x) => x.status === "fulfilled")
274
+ .map((x) => (x as PromiseFulfilledResult<CustomTokenInfo>).value);
275
+
276
+ this.update({ tokens: { [this.userSelectedAddress]: mergeTokenArrays(this.userTokens, nonZeroTokens) } });
277
+ }
278
+ }
@@ -0,0 +1,152 @@
1
+ // import assert from 'assert'
2
+ import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
3
+ import { Mutex, MutexInterface } from "async-mutex";
4
+
5
+ import PollingBlockTracker from "../Block/PollingBlockTracker";
6
+ import { METHOD_TYPES } from "../utils/constants";
7
+ import { EthereumTransactionMeta, Nonce, NonceDetails, NonceLockRes } from "../utils/interfaces";
8
+ import TransactionStateManager from "./TransactionStateManager";
9
+
10
+ interface INonceTrackerOptions {
11
+ provider: SafeEventEmitterProvider;
12
+ blockTracker: PollingBlockTracker;
13
+ getPendingTransactions: TransactionStateManager["getPendingTransactions"];
14
+ getConfirmedTransactions: TransactionStateManager["getConfirmedTransactions"];
15
+ }
16
+
17
+ class NonceTracker {
18
+ private provider: SafeEventEmitterProvider;
19
+
20
+ private blockTracker: PollingBlockTracker;
21
+
22
+ private getPendingTransactions: TransactionStateManager["getPendingTransactions"];
23
+
24
+ private getConfirmedTransactions: TransactionStateManager["getConfirmedTransactions"];
25
+
26
+ private lockMap: Record<string, Mutex>;
27
+
28
+ constructor({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }: INonceTrackerOptions) {
29
+ this.provider = provider;
30
+ this.blockTracker = blockTracker;
31
+ this.getPendingTransactions = getPendingTransactions;
32
+ this.getConfirmedTransactions = getConfirmedTransactions;
33
+ this.lockMap = {};
34
+ }
35
+
36
+ public async getGlobalLock(): Promise<{ releaseLock: MutexInterface.Releaser }> {
37
+ const globalMutex = this._lookupMutex("global");
38
+ // await global mutex free
39
+ const releaseLock = await globalMutex.acquire();
40
+ return { releaseLock };
41
+ }
42
+
43
+ /**
44
+ this will return an object with the `nextNonce`
45
+ `nonceDetails`, and the releaseLock.
46
+ Note: releaseLock must be called after adding a signed tx
47
+ to pending transactions (or discarding).
48
+ */
49
+ public async getNonceLock(address: string): Promise<NonceLockRes> {
50
+ // await global mutex free
51
+ await this._globalMutexFree();
52
+ // await lock free, then take lock
53
+ const releaseLock = await this._takeMutex(address);
54
+ try {
55
+ // evaluate multiple nextNonce strategies
56
+ const nonceDetails = {} as NonceDetails;
57
+ const networkNonceResult = await this._getNetworkNextNonce(address);
58
+
59
+ const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address);
60
+ const nextNetworkNonce = networkNonceResult.nonce;
61
+ const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed);
62
+
63
+ const pendingTxs = this.getPendingTransactions(address);
64
+ const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested);
65
+
66
+ nonceDetails.params = {
67
+ highestLocallyConfirmed,
68
+ highestSuggested,
69
+ nextNetworkNonce,
70
+ };
71
+ nonceDetails.local = localNonceResult;
72
+ nonceDetails.network = networkNonceResult;
73
+
74
+ const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce);
75
+
76
+ // return nonce and release cb
77
+ return { nextNonce, nonceDetails, releaseLock };
78
+ } catch (error) {
79
+ // release lock if we encounter an error
80
+ releaseLock();
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ private async _globalMutexFree() {
86
+ const globalMutex = this._lookupMutex("global");
87
+ const releaseLock = await globalMutex.acquire();
88
+ releaseLock();
89
+ }
90
+
91
+ private async _takeMutex(lockId: string) {
92
+ const mutex = this._lookupMutex(lockId);
93
+ const releaseLock = await mutex.acquire();
94
+ return releaseLock;
95
+ }
96
+
97
+ private _lookupMutex(lockId: string): Mutex {
98
+ let mutex = this.lockMap[lockId];
99
+ if (!mutex) {
100
+ mutex = new Mutex();
101
+ this.lockMap[lockId] = mutex;
102
+ }
103
+ return mutex;
104
+ }
105
+
106
+ private async _getNetworkNextNonce(address: string) {
107
+ // calculate next nonce
108
+ // we need to make sure our base count
109
+ // and pending count are from the same block
110
+ const block = await this.blockTracker.getLatestBlock();
111
+ const baseCountStr = await this.provider.request<[string, string], string>({
112
+ method: METHOD_TYPES.ETH_GET_TRANSACTION_COUNT,
113
+ params: [address, block.idempotencyKey],
114
+ });
115
+ const baseCount = Number.parseInt(baseCountStr, 16);
116
+ const nonceDetails = { block, baseCount };
117
+ return { name: "network", nonce: baseCount, details: nonceDetails };
118
+ }
119
+
120
+ private _getHighestLocallyConfirmed(address: string): number {
121
+ const confirmedTransactions = this.getConfirmedTransactions(address);
122
+ const highest = this._getHighestNonce(confirmedTransactions);
123
+ return Number.isInteger(highest) ? highest + 1 : 0;
124
+ }
125
+
126
+ private _getHighestNonce(txList: EthereumTransactionMeta[]): number {
127
+ const nonces = txList.map((txMeta) => {
128
+ const { nonce } = txMeta.transaction;
129
+ return Number.parseInt(nonce, 16);
130
+ });
131
+ const highestNonce = Math.max.apply(null, nonces);
132
+ return highestNonce;
133
+ }
134
+
135
+ private _getHighestContinuousFrom(txList: EthereumTransactionMeta[], startPoint: number): Nonce {
136
+ const nonces = new Set(
137
+ txList.map((txMeta) => {
138
+ const { nonce } = txMeta.transaction;
139
+ return Number.parseInt(nonce, 16);
140
+ })
141
+ );
142
+
143
+ let highest = startPoint;
144
+ while (nonces.has(highest)) {
145
+ highest += 1;
146
+ }
147
+
148
+ return { name: "local", nonce: highest, details: { startPoint, highest } };
149
+ }
150
+ }
151
+
152
+ export default NonceTracker;