@toruslabs/ethereum-controllers 5.5.3 → 5.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ethereumControllers.cjs.js +3 -12
- package/dist/ethereumControllers.esm.js +2 -11
- package/dist/ethereumControllers.umd.min.js +1 -2
- package/dist/ethereumControllers.umd.min.js.LICENSE.txt +0 -2
- package/dist/types/Account/AccountTrackerController.d.ts +1 -1
- package/package.json +14 -15
- package/dist/ethereumControllers.cjs.js.map +0 -1
- package/dist/ethereumControllers.esm.js.map +0 -1
- package/dist/ethereumControllers.umd.min.js.map +0 -1
- package/src/Account/AccountTrackerController.ts +0 -172
- package/src/Block/PollingBlockTracker.ts +0 -89
- package/src/Currency/CurrencyController.ts +0 -117
- package/src/Gas/GasFeeController.ts +0 -261
- package/src/Gas/IGasFeeController.ts +0 -56
- package/src/Gas/gasUtil.ts +0 -163
- package/src/Keyring/KeyringController.ts +0 -117
- package/src/Message/AbstractMessageController.ts +0 -136
- package/src/Message/AddChainController.ts +0 -73
- package/src/Message/DecryptMessageController.ts +0 -76
- package/src/Message/EncryptionPublicKeyController.ts +0 -75
- package/src/Message/MessageController.ts +0 -74
- package/src/Message/PersonalMessageController.ts +0 -74
- package/src/Message/SwitchChainController.ts +0 -74
- package/src/Message/TypedMessageController.ts +0 -109
- package/src/Message/utils.ts +0 -155
- package/src/Network/NetworkController.ts +0 -184
- package/src/Network/createEthereumMiddleware.ts +0 -475
- package/src/Network/createJsonRpcClient.ts +0 -63
- package/src/Nfts/INftsController.ts +0 -13
- package/src/Nfts/NftHandler.ts +0 -191
- package/src/Nfts/NftsController.ts +0 -216
- package/src/Preferences/PreferencesController.ts +0 -473
- package/src/Tokens/ITokensController.ts +0 -13
- package/src/Tokens/TokenHandler.ts +0 -60
- package/src/Tokens/TokenRatesController.ts +0 -134
- package/src/Tokens/TokensController.ts +0 -273
- package/src/Transaction/NonceTracker.ts +0 -152
- package/src/Transaction/PendingTransactionTracker.ts +0 -235
- package/src/Transaction/TransactionController.ts +0 -558
- package/src/Transaction/TransactionGasUtil.ts +0 -74
- package/src/Transaction/TransactionStateHistoryHelper.ts +0 -41
- package/src/Transaction/TransactionStateManager.ts +0 -315
- package/src/Transaction/TransactionUtils.ts +0 -333
- package/src/index.ts +0 -49
- package/src/utils/abis.ts +0 -677
- package/src/utils/constants.ts +0 -438
- package/src/utils/contractAddresses.ts +0 -19
- package/src/utils/conversionUtils.ts +0 -269
- package/src/utils/helpers.ts +0 -245
- package/src/utils/interfaces.ts +0 -519
|
@@ -1,273 +0,0 @@
|
|
|
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
|
-
getCustomTokens?: PreferencesController["getCustomTokens"];
|
|
21
|
-
getEtherScanTokens: PreferencesController["getEtherScanTokens"];
|
|
22
|
-
getProviderConfig: NetworkController["getProviderConfig"];
|
|
23
|
-
onPreferencesStateChange: (listener: (preferencesState: PreferencesState<ExtendedAddressPreferences>) => void) => void;
|
|
24
|
-
onNetworkStateChange: (listener: (networkState: EthereumNetworkState) => void) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getObjectFromArrayBasedonKey(oldArray: CustomTokenInfo[], key: string) {
|
|
28
|
-
return oldArray.reduce((acc: Record<string, CustomTokenInfo>, x) => {
|
|
29
|
-
const xkey = x[key as keyof CustomTokenInfo];
|
|
30
|
-
if (typeof xkey === "boolean") return acc;
|
|
31
|
-
acc[xkey] = x;
|
|
32
|
-
return acc;
|
|
33
|
-
}, {});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const mergeTokenArrays = (oldArray: CustomTokenInfo[], newArray: CustomTokenInfo[]): CustomTokenInfo[] => {
|
|
37
|
-
const oldMap = getObjectFromArrayBasedonKey(oldArray || [], "tokenAddress");
|
|
38
|
-
const newMap = getObjectFromArrayBasedonKey(newArray || [], "tokenAddress");
|
|
39
|
-
const finalArr = newArray;
|
|
40
|
-
Object.keys(oldMap).forEach((x) => {
|
|
41
|
-
if (!newMap[x] && oldMap[x].isEtherScan) finalArr.push(oldMap[x]);
|
|
42
|
-
});
|
|
43
|
-
return finalArr;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const DEFAULT_INTERVAL = 180 * 1000;
|
|
47
|
-
|
|
48
|
-
export class TokensController extends BaseController<TokensControllerConfig, TokensControllerState> {
|
|
49
|
-
name = "TokensController";
|
|
50
|
-
|
|
51
|
-
private provider: SafeEventEmitterProvider;
|
|
52
|
-
|
|
53
|
-
private ethersProvider: BrowserProvider;
|
|
54
|
-
|
|
55
|
-
private _timer: number;
|
|
56
|
-
|
|
57
|
-
private getProviderConfig: NetworkController["getProviderConfig"];
|
|
58
|
-
|
|
59
|
-
private getCustomTokens: PreferencesController["getCustomTokens"];
|
|
60
|
-
|
|
61
|
-
private getEtherScanTokens: PreferencesController["getEtherScanTokens"];
|
|
62
|
-
|
|
63
|
-
constructor({
|
|
64
|
-
config,
|
|
65
|
-
state,
|
|
66
|
-
provider,
|
|
67
|
-
getCustomTokens,
|
|
68
|
-
getEtherScanTokens,
|
|
69
|
-
getProviderConfig,
|
|
70
|
-
onPreferencesStateChange,
|
|
71
|
-
onNetworkStateChange,
|
|
72
|
-
}: ITokensControllerOptions) {
|
|
73
|
-
super({ config, state });
|
|
74
|
-
|
|
75
|
-
this.provider = provider;
|
|
76
|
-
this.ethersProvider = new BrowserProvider(this.provider, "any");
|
|
77
|
-
|
|
78
|
-
this.getCustomTokens = getCustomTokens;
|
|
79
|
-
this.getEtherScanTokens = getEtherScanTokens;
|
|
80
|
-
this.getProviderConfig = getProviderConfig;
|
|
81
|
-
|
|
82
|
-
this.defaultConfig = {
|
|
83
|
-
interval: DEFAULT_INTERVAL,
|
|
84
|
-
selectedAddress: "",
|
|
85
|
-
chainId: "",
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
this.defaultState = {
|
|
89
|
-
tokens: {},
|
|
90
|
-
};
|
|
91
|
-
this.initialize();
|
|
92
|
-
|
|
93
|
-
onPreferencesStateChange((preferencesState) => {
|
|
94
|
-
if (preferencesState.selectedAddress !== this.config.selectedAddress) {
|
|
95
|
-
this.configure({ selectedAddress: preferencesState.selectedAddress });
|
|
96
|
-
this.restartTokenDetection();
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
onNetworkStateChange((networkState) => {
|
|
101
|
-
const { chainId } = networkState.providerConfig;
|
|
102
|
-
if (chainId !== this.config.chainId) {
|
|
103
|
-
this.configure({ chainId });
|
|
104
|
-
this.restartTokenDetection();
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
get userSelectedAddress(): string {
|
|
110
|
-
return this.config.selectedAddress;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
get userTokens() {
|
|
114
|
-
if (!this.userSelectedAddress) return [];
|
|
115
|
-
return this.state.tokens[this.userSelectedAddress] ?? [];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
get interval(): number {
|
|
119
|
-
return this.config.interval;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
set interval(interval: number) {
|
|
123
|
-
if (this._timer) window.clearInterval(this._timer);
|
|
124
|
-
if (!interval) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
this._timer = window.setInterval(() => {
|
|
128
|
-
if (!idleTimeTracker.checkIfIdle()) {
|
|
129
|
-
this.detectNewTokens();
|
|
130
|
-
this.refreshTokenBalances();
|
|
131
|
-
}
|
|
132
|
-
}, interval);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
public startTokenDetection(selectedAddress: string) {
|
|
136
|
-
this.configure({ selectedAddress });
|
|
137
|
-
this.restartTokenDetection();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Restart token detection polling period and call detectNewTokens
|
|
142
|
-
* in case of address change or user session initialization.
|
|
143
|
-
*
|
|
144
|
-
*/
|
|
145
|
-
public restartTokenDetection() {
|
|
146
|
-
if (!this.userSelectedAddress) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
this.detectNewTokens();
|
|
150
|
-
this.refreshTokenBalances();
|
|
151
|
-
this.config.interval = DEFAULT_INTERVAL;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
public detectNewTokens() {
|
|
155
|
-
const userAddress = this.userSelectedAddress;
|
|
156
|
-
if (!userAddress) return;
|
|
157
|
-
const currentChainId = this.config.chainId;
|
|
158
|
-
const tokens: CustomTokenInfo[] = []; // object[]
|
|
159
|
-
if (!currentChainId) {
|
|
160
|
-
this.update({ tokens: { [userAddress]: [...tokens] } });
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const networkConfig = this.getProviderConfig();
|
|
165
|
-
|
|
166
|
-
if (networkConfig?.isErc20 && networkConfig?.tokenAddress) {
|
|
167
|
-
tokens.push({
|
|
168
|
-
tokenAddress: networkConfig.tokenAddress,
|
|
169
|
-
name: networkConfig.tickerName,
|
|
170
|
-
logo: networkConfig.logo,
|
|
171
|
-
erc20: true,
|
|
172
|
-
symbol: networkConfig.ticker,
|
|
173
|
-
decimals: "18",
|
|
174
|
-
chainId: currentChainId,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
if (this.getCustomTokens) {
|
|
178
|
-
const customTokens = this.getCustomTokens(userAddress);
|
|
179
|
-
tokens.push(
|
|
180
|
-
...customTokens.reduce((acc, x) => {
|
|
181
|
-
if (x.network === currentChainId)
|
|
182
|
-
acc.push({
|
|
183
|
-
tokenAddress: x.token_address,
|
|
184
|
-
name: x.token_name,
|
|
185
|
-
logo: "eth.svg",
|
|
186
|
-
erc20: true,
|
|
187
|
-
symbol: x.token_symbol,
|
|
188
|
-
decimals: x.decimals,
|
|
189
|
-
balance: "",
|
|
190
|
-
customTokenId: x.id.toString(),
|
|
191
|
-
chainId: x.network,
|
|
192
|
-
});
|
|
193
|
-
return acc;
|
|
194
|
-
}, [] as CustomTokenInfo[])
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
this.update({ tokens: { [userAddress]: [...tokens] } });
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async refreshTokenBalances() {
|
|
201
|
-
const userAddress = this.userSelectedAddress;
|
|
202
|
-
if (userAddress === "") return;
|
|
203
|
-
const oldTokens = [...this.userTokens];
|
|
204
|
-
const tokenAddresses = oldTokens.map((x) => x.tokenAddress);
|
|
205
|
-
const nonZeroTokens: CustomTokenInfo[] = [];
|
|
206
|
-
try {
|
|
207
|
-
const currentChainId = this.config.chainId;
|
|
208
|
-
if (ETHERSCAN_SUPPORTED_CHAINS.includes(currentChainId)) {
|
|
209
|
-
const etherscanBalances = await this.getEtherScanTokens(userAddress, currentChainId);
|
|
210
|
-
nonZeroTokens.push(...etherscanBalances);
|
|
211
|
-
}
|
|
212
|
-
if (tokenAddresses.length > 0) {
|
|
213
|
-
const currentSingleCallAddress = SINGLE_CALL_BALANCES_ADDRESSES[currentChainId];
|
|
214
|
-
if (currentSingleCallAddress) {
|
|
215
|
-
const ethContract = new Contract(currentSingleCallAddress, singleBalanceCheckerAbi, this.ethersProvider);
|
|
216
|
-
const result = await ethContract.balances([userAddress], tokenAddresses);
|
|
217
|
-
tokenAddresses.forEach((_, index) => {
|
|
218
|
-
const balance = toQuantity(result[index]);
|
|
219
|
-
if (balance && balance !== "0x0") {
|
|
220
|
-
nonZeroTokens.push({ ...oldTokens[index], balance, chainId: currentChainId });
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
} else {
|
|
224
|
-
this.getTokenBalancesUsingHandler(oldTokens);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
} catch (error) {
|
|
228
|
-
log.error(error, "unable to fetch token balances using single call balance address");
|
|
229
|
-
this.getTokenBalancesUsingHandler(oldTokens);
|
|
230
|
-
} finally {
|
|
231
|
-
this.update({ tokens: { [userAddress]: nonZeroTokens } });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async getTokenBalancesUsingHandler(customTokens: CustomTokenInfo[]) {
|
|
236
|
-
if (!this.userSelectedAddress) return;
|
|
237
|
-
const currentNetworkTokens = customTokens;
|
|
238
|
-
const promiseSettledResult = await Promise.allSettled(
|
|
239
|
-
currentNetworkTokens.map(async (x) => {
|
|
240
|
-
try {
|
|
241
|
-
const tokenInstance = new TokenHandler({
|
|
242
|
-
address: x.tokenAddress,
|
|
243
|
-
decimals: Number.parseInt(x.decimals),
|
|
244
|
-
name: x.name,
|
|
245
|
-
symbol: x.symbol,
|
|
246
|
-
provider: this.ethersProvider,
|
|
247
|
-
});
|
|
248
|
-
const balance = await tokenInstance.getUserBalance(this.userSelectedAddress);
|
|
249
|
-
return {
|
|
250
|
-
decimals: tokenInstance.decimals.toString(),
|
|
251
|
-
erc20: true,
|
|
252
|
-
logo: x.logo || "eth.svg",
|
|
253
|
-
name: tokenInstance.name,
|
|
254
|
-
symbol: tokenInstance.symbol,
|
|
255
|
-
tokenAddress: toChecksumAddressByChainId(tokenInstance.address, x.chainId),
|
|
256
|
-
balance: `0x${balance}`,
|
|
257
|
-
customTokenId: x.customTokenId,
|
|
258
|
-
network: x.chainId,
|
|
259
|
-
chainId: x.chainId,
|
|
260
|
-
} as CustomTokenInfo;
|
|
261
|
-
} catch (error) {
|
|
262
|
-
log.warn("Invalid contract address while fetching", error);
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
);
|
|
267
|
-
const nonZeroTokens = promiseSettledResult
|
|
268
|
-
.filter((x) => x.status === "fulfilled")
|
|
269
|
-
.map((x) => (x as PromiseFulfilledResult<CustomTokenInfo>).value);
|
|
270
|
-
|
|
271
|
-
this.update({ tokens: { [this.userSelectedAddress]: mergeTokenArrays(this.userTokens, nonZeroTokens) } });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
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;
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BASE_TX_EVENT_TYPE,
|
|
3
|
-
ITransactionController,
|
|
4
|
-
TransactionStatus,
|
|
5
|
-
TX_CONFIRMED_EVENT_TYPE,
|
|
6
|
-
TX_DROPPED_EVENT_TYPE,
|
|
7
|
-
TX_EVENTS,
|
|
8
|
-
TX_FAILED_EVENT_TYPE,
|
|
9
|
-
TX_WARNING_EVENT_TYPE,
|
|
10
|
-
} from "@toruslabs/base-controllers";
|
|
11
|
-
import { SafeEventEmitter, SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
|
|
12
|
-
import log from "loglevel";
|
|
13
|
-
|
|
14
|
-
import { METHOD_TYPES } from "../utils/constants";
|
|
15
|
-
import { EthereumBlock, EthereumTransactionMeta, TransactionParams, TransactionReceipt } from "../utils/interfaces";
|
|
16
|
-
import NonceTracker from "./NonceTracker";
|
|
17
|
-
import TransactionStateManager from "./TransactionStateManager";
|
|
18
|
-
|
|
19
|
-
export default class PendingTransactionTracker extends SafeEventEmitter {
|
|
20
|
-
DROPPED_BUFFER_COUNT = 3;
|
|
21
|
-
|
|
22
|
-
private nonceTracker: NonceTracker;
|
|
23
|
-
|
|
24
|
-
private provider: SafeEventEmitterProvider;
|
|
25
|
-
|
|
26
|
-
private approveTransaction: ITransactionController<EthereumTransactionMeta>["approveTransaction"];
|
|
27
|
-
|
|
28
|
-
private droppedBlocksBufferByHash: Map<string, number>;
|
|
29
|
-
|
|
30
|
-
private getConfirmedTransactions: TransactionStateManager["getConfirmedTransactions"];
|
|
31
|
-
|
|
32
|
-
private getPendingTransactions: TransactionStateManager["getPendingTransactions"];
|
|
33
|
-
|
|
34
|
-
private publishTransaction: (rawTx: string) => Promise<string>;
|
|
35
|
-
|
|
36
|
-
constructor({
|
|
37
|
-
provider,
|
|
38
|
-
nonceTracker,
|
|
39
|
-
approveTransaction,
|
|
40
|
-
publishTransaction,
|
|
41
|
-
getPendingTransactions,
|
|
42
|
-
getConfirmedTransactions,
|
|
43
|
-
}: {
|
|
44
|
-
provider: SafeEventEmitterProvider;
|
|
45
|
-
nonceTracker: NonceTracker;
|
|
46
|
-
approveTransaction: ITransactionController<EthereumTransactionMeta>["approveTransaction"];
|
|
47
|
-
publishTransaction: (rawTx: string) => Promise<string>;
|
|
48
|
-
getPendingTransactions: TransactionStateManager["getPendingTransactions"];
|
|
49
|
-
getConfirmedTransactions: TransactionStateManager["getConfirmedTransactions"];
|
|
50
|
-
}) {
|
|
51
|
-
super();
|
|
52
|
-
this.provider = provider;
|
|
53
|
-
this.nonceTracker = nonceTracker;
|
|
54
|
-
this.approveTransaction = approveTransaction;
|
|
55
|
-
this.publishTransaction = publishTransaction;
|
|
56
|
-
this.getPendingTransactions = getPendingTransactions;
|
|
57
|
-
this.getConfirmedTransactions = getConfirmedTransactions;
|
|
58
|
-
this.droppedBlocksBufferByHash = new Map();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
checks the network for signed txs and releases the nonce global lock if it is
|
|
63
|
-
*/
|
|
64
|
-
public async updatePendingTxs(): Promise<void> {
|
|
65
|
-
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
|
66
|
-
const nonceGlobalLock = await this.nonceTracker.getGlobalLock();
|
|
67
|
-
try {
|
|
68
|
-
const pendingTxs = this.getPendingTransactions();
|
|
69
|
-
await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)));
|
|
70
|
-
} catch (error) {
|
|
71
|
-
log.error("PendingTransactionTracker - Error updating pending transactions");
|
|
72
|
-
log.error(error);
|
|
73
|
-
}
|
|
74
|
-
nonceGlobalLock.releaseLock();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
public async resubmitPendingTxs(block: EthereumBlock) {
|
|
78
|
-
const pending = this.getPendingTransactions();
|
|
79
|
-
// only try resubmitting if their are transactions to resubmit
|
|
80
|
-
if (pending.length === 0) return;
|
|
81
|
-
// Keep this as a for loop because we want to wait for each item to be submitted
|
|
82
|
-
for (const txMeta of pending) {
|
|
83
|
-
try {
|
|
84
|
-
await this._resubmitTx(txMeta, block.idempotencyKey);
|
|
85
|
-
} catch (error: unknown) {
|
|
86
|
-
/*
|
|
87
|
-
Dont marked as failed if the error is a "known" transaction warning
|
|
88
|
-
"there is already a transaction with the same sender-nonce
|
|
89
|
-
but higher/same gas price"
|
|
90
|
-
|
|
91
|
-
Also don't mark as failed if it has ever been broadcast successfully.
|
|
92
|
-
A successful broadcast means it may still be mined.
|
|
93
|
-
*/
|
|
94
|
-
const errorMessage = (error as { value: Error }).value?.message?.toLowerCase() || (error as Error).message.toLowerCase();
|
|
95
|
-
const isKnownTx =
|
|
96
|
-
// geth
|
|
97
|
-
errorMessage.includes("replacement transaction underpriced") ||
|
|
98
|
-
errorMessage.includes("known transaction") ||
|
|
99
|
-
// parity
|
|
100
|
-
errorMessage.includes("gas price too low to replace") ||
|
|
101
|
-
errorMessage.includes("transaction with the same hash was already imported") ||
|
|
102
|
-
// other
|
|
103
|
-
errorMessage.includes("gateway timeout") ||
|
|
104
|
-
errorMessage.includes("nonce too low");
|
|
105
|
-
// ignore resubmit warnings, return early
|
|
106
|
-
if (isKnownTx) return;
|
|
107
|
-
// encountered real error - transition to error state
|
|
108
|
-
txMeta.warning = {
|
|
109
|
-
error: errorMessage,
|
|
110
|
-
message: "There was an error when resubmitting this transaction.",
|
|
111
|
-
};
|
|
112
|
-
this.emit(TX_EVENTS.TX_WARNING, { txMeta, error, txId: txMeta.id } as TX_WARNING_EVENT_TYPE<TransactionParams, EthereumTransactionMeta>);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async _resubmitTx(txMeta: EthereumTransactionMeta, latestBlockNumber?: string) {
|
|
118
|
-
if (!txMeta.firstRetryBlockNumber) {
|
|
119
|
-
this.emit(TX_EVENTS.TX_BLOCK_UPDATE, { txMeta, latestBlockNumber, txId: txMeta.id } as BASE_TX_EVENT_TYPE);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber;
|
|
123
|
-
const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16);
|
|
124
|
-
|
|
125
|
-
const retryCount = txMeta.retryCount || 0;
|
|
126
|
-
|
|
127
|
-
// Exponential backoff to limit retries at publishing (capped at last 15 mins)
|
|
128
|
-
if (txBlockDistance <= Math.min(50, 2 ** retryCount)) return undefined;
|
|
129
|
-
|
|
130
|
-
// Only auto-submit already-signed txs:
|
|
131
|
-
if (!("rawTx" in txMeta)) return this.approveTransaction(txMeta.id);
|
|
132
|
-
|
|
133
|
-
const { rawTx } = txMeta;
|
|
134
|
-
const txHash = await this.publishTransaction(rawTx as string);
|
|
135
|
-
|
|
136
|
-
// Increment successful tries:
|
|
137
|
-
this.emit(TX_EVENTS.TX_RETRY, { txMeta, txId: txMeta.id } as BASE_TX_EVENT_TYPE);
|
|
138
|
-
return txHash;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async _checkPendingTx(foundTx: EthereumTransactionMeta): Promise<void> {
|
|
142
|
-
const txMeta = foundTx;
|
|
143
|
-
const txHash = txMeta.transactionHash;
|
|
144
|
-
const txId = txMeta.id;
|
|
145
|
-
|
|
146
|
-
// Only check submitted txs
|
|
147
|
-
if (txMeta.status !== TransactionStatus.submitted) return;
|
|
148
|
-
|
|
149
|
-
// extra check in case there was an uncaught error during the
|
|
150
|
-
// signature and submission process
|
|
151
|
-
if (!txHash) {
|
|
152
|
-
const noTxHashError = new Error("We had an error while submitting this transaction, please try again.");
|
|
153
|
-
noTxHashError.name = "NoTxHashError";
|
|
154
|
-
this.emit(TX_EVENTS.TX_FAILED, { txId, error: noTxHashError } as TX_FAILED_EVENT_TYPE);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// If another tx with the same nonce is mined, set as failed.
|
|
159
|
-
if (this._checkIfNonceIsTaken(txMeta)) {
|
|
160
|
-
this.emit(TX_EVENTS.TX_DROPPED, { txId } as TX_DROPPED_EVENT_TYPE);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const transactionReceipt = await this.provider.request<[string], TransactionReceipt>({
|
|
166
|
-
method: METHOD_TYPES.ETH_GET_TRANSACTION_RECEIPT,
|
|
167
|
-
params: [txHash],
|
|
168
|
-
});
|
|
169
|
-
if (transactionReceipt?.blockNumber) {
|
|
170
|
-
const { baseFeePerGas, timestamp } = await this.provider.request<[string, boolean], EthereumBlock>({
|
|
171
|
-
method: METHOD_TYPES.ETH_GET_BLOCK_BY_HASH,
|
|
172
|
-
params: [transactionReceipt.blockHash, false],
|
|
173
|
-
});
|
|
174
|
-
this.emit(TX_EVENTS.TX_CONFIRMED, {
|
|
175
|
-
txId,
|
|
176
|
-
txReceipt: transactionReceipt,
|
|
177
|
-
baseFeePerGas,
|
|
178
|
-
blockTimestamp: timestamp,
|
|
179
|
-
} as TX_CONFIRMED_EVENT_TYPE);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
} catch (error) {
|
|
183
|
-
log.error("error while loading tx", error);
|
|
184
|
-
txMeta.warning = {
|
|
185
|
-
error: (error as Error).message,
|
|
186
|
-
message: "There was a problem loading this transaction.",
|
|
187
|
-
};
|
|
188
|
-
this.emit(TX_EVENTS.TX_WARNING, { txMeta } as TX_WARNING_EVENT_TYPE<TransactionParams, EthereumTransactionMeta>);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (await this._checkIfTxWasDropped(txMeta)) {
|
|
192
|
-
this.emit(TX_EVENTS.TX_DROPPED, { txId } as TX_DROPPED_EVENT_TYPE);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async _checkIfTxWasDropped(txMeta: EthereumTransactionMeta): Promise<boolean> {
|
|
197
|
-
const {
|
|
198
|
-
transactionHash: txHash,
|
|
199
|
-
transaction: { nonce, from },
|
|
200
|
-
} = txMeta;
|
|
201
|
-
const networkNextNonce = await this.provider.request<[string, string], string>({
|
|
202
|
-
method: METHOD_TYPES.ETH_GET_TRANSACTION_COUNT,
|
|
203
|
-
params: [from, "latest"],
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
if (Number.parseInt(nonce, 16) >= Number.parseInt(networkNextNonce, 16)) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!this.droppedBlocksBufferByHash.has(txHash)) {
|
|
211
|
-
this.droppedBlocksBufferByHash.set(txHash, 0);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const currentBlockBuffer = this.droppedBlocksBufferByHash.get(txHash);
|
|
215
|
-
|
|
216
|
-
if (currentBlockBuffer < this.DROPPED_BUFFER_COUNT) {
|
|
217
|
-
this.droppedBlocksBufferByHash.set(txHash, currentBlockBuffer + 1);
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.droppedBlocksBufferByHash.delete(txHash);
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
_checkIfNonceIsTaken(txMeta: EthereumTransactionMeta) {
|
|
226
|
-
const address = txMeta.transaction.from;
|
|
227
|
-
const completed = this.getConfirmedTransactions(address);
|
|
228
|
-
return completed.some((otherMeta) => {
|
|
229
|
-
if (otherMeta.id === txMeta.id) {
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
return otherMeta.transaction.nonce === txMeta.transaction.nonce;
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|