@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.
- package/dist/ethereumControllers.cjs.js +6153 -0
- package/dist/ethereumControllers.cjs.js.map +1 -0
- package/dist/ethereumControllers.esm.js +5570 -0
- package/dist/ethereumControllers.esm.js.map +1 -0
- package/dist/ethereumControllers.umd.min.js +3 -0
- package/dist/ethereumControllers.umd.min.js.LICENSE.txt +38 -0
- package/dist/ethereumControllers.umd.min.js.map +1 -0
- package/dist/types/Account/AccountTrackerController.d.ts +35 -0
- package/dist/types/Block/PollingBlockTracker.d.ts +14 -0
- package/dist/types/Currency/CurrencyController.d.ts +30 -0
- package/dist/types/Gas/GasFeeController.d.ts +64 -0
- package/dist/types/Gas/IGasFeeController.d.ts +49 -0
- package/dist/types/Gas/gasUtil.d.ts +21 -0
- package/dist/types/Keyring/KeyringController.d.ts +20 -0
- package/dist/types/Message/AbstractMessageController.d.ts +36 -0
- package/dist/types/Message/DecryptMessageController.d.ts +20 -0
- package/dist/types/Message/EncryptionPublicKeyController.d.ts +20 -0
- package/dist/types/Message/MessageController.d.ts +20 -0
- package/dist/types/Message/PersonalMessageController.d.ts +20 -0
- package/dist/types/Message/TypedMessageController.d.ts +21 -0
- package/dist/types/Message/utils.d.ts +10 -0
- package/dist/types/Network/NetworkController.d.ts +40 -0
- package/dist/types/Network/createEthereumMiddleware.d.ts +66 -0
- package/dist/types/Network/createJsonRpcClient.d.ts +9 -0
- package/dist/types/Nfts/INftsController.d.ts +10 -0
- package/dist/types/Nfts/NftHandler.d.ts +35 -0
- package/dist/types/Nfts/NftsController.d.ts +40 -0
- package/dist/types/Preferences/PreferencesController.d.ts +53 -0
- package/dist/types/Tokens/ITokensController.d.ts +10 -0
- package/dist/types/Tokens/TokenHandler.d.ts +20 -0
- package/dist/types/Tokens/TokenRatesController.d.ts +42 -0
- package/dist/types/Tokens/TokensController.d.ts +42 -0
- package/dist/types/Transaction/NonceTracker.d.ts +37 -0
- package/dist/types/Transaction/PendingTransactionTracker.d.ts +32 -0
- package/dist/types/Transaction/TransactionController.d.ts +67 -0
- package/dist/types/Transaction/TransactionGasUtil.d.ts +21 -0
- package/dist/types/Transaction/TransactionStateHistoryHelper.d.ts +16 -0
- package/dist/types/Transaction/TransactionStateManager.d.ts +30 -0
- package/dist/types/Transaction/TransactionUtils.d.ts +70 -0
- package/dist/types/index.d.ts +43 -0
- package/dist/types/utils/abiDecoder.d.ts +17 -0
- package/dist/types/utils/abis.d.ts +84 -0
- package/dist/types/utils/constants.d.ts +81 -0
- package/dist/types/utils/contractAddresses.d.ts +1 -0
- package/dist/types/utils/conversionUtils.d.ts +42 -0
- package/dist/types/utils/helpers.d.ts +24 -0
- package/dist/types/utils/interfaces.d.ts +384 -0
- package/package.json +71 -0
- package/src/Account/AccountTrackerController.ts +157 -0
- package/src/Block/PollingBlockTracker.ts +89 -0
- package/src/Currency/CurrencyController.ts +117 -0
- package/src/Gas/GasFeeController.ts +254 -0
- package/src/Gas/IGasFeeController.ts +56 -0
- package/src/Gas/gasUtil.ts +163 -0
- package/src/Keyring/KeyringController.ts +118 -0
- package/src/Message/AbstractMessageController.ts +136 -0
- package/src/Message/DecryptMessageController.ts +81 -0
- package/src/Message/EncryptionPublicKeyController.ts +83 -0
- package/src/Message/MessageController.ts +74 -0
- package/src/Message/PersonalMessageController.ts +74 -0
- package/src/Message/TypedMessageController.ts +112 -0
- package/src/Message/utils.ts +107 -0
- package/src/Network/NetworkController.ts +184 -0
- package/src/Network/createEthereumMiddleware.ts +307 -0
- package/src/Network/createJsonRpcClient.ts +59 -0
- package/src/Nfts/INftsController.ts +13 -0
- package/src/Nfts/NftHandler.ts +191 -0
- package/src/Nfts/NftsController.ts +230 -0
- package/src/Preferences/PreferencesController.ts +409 -0
- package/src/Tokens/ITokensController.ts +13 -0
- package/src/Tokens/TokenHandler.ts +60 -0
- package/src/Tokens/TokenRatesController.ts +134 -0
- package/src/Tokens/TokensController.ts +278 -0
- package/src/Transaction/NonceTracker.ts +152 -0
- package/src/Transaction/PendingTransactionTracker.ts +235 -0
- package/src/Transaction/TransactionController.ts +558 -0
- package/src/Transaction/TransactionGasUtil.ts +74 -0
- package/src/Transaction/TransactionStateHistoryHelper.ts +41 -0
- package/src/Transaction/TransactionStateManager.ts +315 -0
- package/src/Transaction/TransactionUtils.ts +333 -0
- package/src/index.ts +45 -0
- package/src/utils/abiDecoder.ts +195 -0
- package/src/utils/abis.ts +677 -0
- package/src/utils/constants.ts +379 -0
- package/src/utils/contractAddresses.ts +21 -0
- package/src/utils/conversionUtils.ts +269 -0
- package/src/utils/helpers.ts +177 -0
- package/src/utils/interfaces.ts +454 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { Common, Hardfork } from "@ethereumjs/common";
|
|
2
|
+
import { TransactionFactory, TypedTransaction } from "@ethereumjs/tx";
|
|
3
|
+
import { addHexPrefix, stripHexPrefix } from "@ethereumjs/util";
|
|
4
|
+
import { providerErrors, rpcErrors } from "@metamask/rpc-errors";
|
|
5
|
+
import {
|
|
6
|
+
ITransactionController,
|
|
7
|
+
TRANSACTION_TYPES,
|
|
8
|
+
TransactionConfig,
|
|
9
|
+
TransactionState,
|
|
10
|
+
TransactionStatus,
|
|
11
|
+
TX_CONFIRMED_EVENT_TYPE,
|
|
12
|
+
TX_DROPPED_EVENT_TYPE,
|
|
13
|
+
TX_EVENTS,
|
|
14
|
+
TX_FAILED_EVENT_TYPE,
|
|
15
|
+
TX_WARNING_EVENT_TYPE,
|
|
16
|
+
} from "@toruslabs/base-controllers";
|
|
17
|
+
import { JRPCRequest, SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
|
|
18
|
+
import BigNumber from "bignumber.js";
|
|
19
|
+
import { keccak256 } from "ethers";
|
|
20
|
+
import log from "loglevel";
|
|
21
|
+
|
|
22
|
+
import PollingBlockTracker from "../Block/PollingBlockTracker";
|
|
23
|
+
import GasFeeController from "../Gas/GasFeeController";
|
|
24
|
+
import { type EthereumGasFeeEstimates, EthereumLegacyGasFeeEstimates } from "../Gas/IGasFeeController";
|
|
25
|
+
import KeyringController from "../Keyring/KeyringController";
|
|
26
|
+
import NetworkController from "../Network/NetworkController";
|
|
27
|
+
import PreferencesController from "../Preferences/PreferencesController";
|
|
28
|
+
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, GAS_ESTIMATE_TYPES, METHOD_TYPES, TRANSACTION_ENVELOPE_TYPES } from "../utils/constants";
|
|
29
|
+
import { decGWEIToHexWEI } from "../utils/conversionUtils";
|
|
30
|
+
import { bnLessThan, GAS_LIMITS, getChainType } from "../utils/helpers";
|
|
31
|
+
import {
|
|
32
|
+
EthereumBlock,
|
|
33
|
+
EthereumTransactionMeta,
|
|
34
|
+
NonceLockRes,
|
|
35
|
+
TransactionParams,
|
|
36
|
+
TransactionReceipt,
|
|
37
|
+
UserRequestApprovalParams,
|
|
38
|
+
} from "../utils/interfaces";
|
|
39
|
+
import NonceTracker from "./NonceTracker";
|
|
40
|
+
import PendingTransactionTracker from "./PendingTransactionTracker";
|
|
41
|
+
import TransactionGasUtil from "./TransactionGasUtil";
|
|
42
|
+
import TransactionStateManager from "./TransactionStateManager";
|
|
43
|
+
import { determineTransactionType, isEIP1559Transaction, normalizeTxParameters, validateTxParameters } from "./TransactionUtils";
|
|
44
|
+
|
|
45
|
+
export default class TransactionController extends TransactionStateManager implements ITransactionController<TransactionParams> {
|
|
46
|
+
getSelectedAddress: PreferencesController["getSelectedAddress"];
|
|
47
|
+
|
|
48
|
+
getEIP1559GasFeeEstimates: GasFeeController["fetchGasFeeEstimates"];
|
|
49
|
+
|
|
50
|
+
public nonceTracker: NonceTracker;
|
|
51
|
+
|
|
52
|
+
public pendingTxTracker: PendingTransactionTracker;
|
|
53
|
+
|
|
54
|
+
public txGasUtil: TransactionGasUtil;
|
|
55
|
+
|
|
56
|
+
private _getCurrentNetworkEIP1559Compatibility: NetworkController["getEIP1559Compatibility"];
|
|
57
|
+
|
|
58
|
+
private _getCurrentAccountEIP1559Compatibility: (address?: string) => Promise<boolean>;
|
|
59
|
+
|
|
60
|
+
private getProviderConfig: NetworkController["getProviderConfig"];
|
|
61
|
+
|
|
62
|
+
private signEthTx: KeyringController["signTransaction"];
|
|
63
|
+
|
|
64
|
+
private provider: SafeEventEmitterProvider;
|
|
65
|
+
|
|
66
|
+
private blockTracker: PollingBlockTracker;
|
|
67
|
+
|
|
68
|
+
private inProcessOfSigning: Set<string> = new Set();
|
|
69
|
+
|
|
70
|
+
constructor({
|
|
71
|
+
config,
|
|
72
|
+
state,
|
|
73
|
+
provider,
|
|
74
|
+
blockTracker,
|
|
75
|
+
signEthTx,
|
|
76
|
+
getCurrentChainId,
|
|
77
|
+
getCurrentNetworkEIP1559Compatibility,
|
|
78
|
+
getProviderConfig,
|
|
79
|
+
getCurrentAccountEIP1559Compatibility,
|
|
80
|
+
getSelectedAddress,
|
|
81
|
+
getEIP1559GasFeeEstimates,
|
|
82
|
+
}: {
|
|
83
|
+
config?: Partial<TransactionConfig>;
|
|
84
|
+
state?: Partial<TransactionState<TransactionParams, EthereumTransactionMeta>>;
|
|
85
|
+
provider: SafeEventEmitterProvider;
|
|
86
|
+
blockTracker: PollingBlockTracker;
|
|
87
|
+
signEthTx: KeyringController["signTransaction"];
|
|
88
|
+
getCurrentChainId: NetworkController["getNetworkIdentifier"];
|
|
89
|
+
getProviderConfig: NetworkController["getProviderConfig"];
|
|
90
|
+
getCurrentNetworkEIP1559Compatibility: NetworkController["getEIP1559Compatibility"];
|
|
91
|
+
getCurrentAccountEIP1559Compatibility: (address?: string) => Promise<boolean>; // used only if keyring supports EIP-1559
|
|
92
|
+
getSelectedAddress: PreferencesController["getSelectedAddress"];
|
|
93
|
+
getEIP1559GasFeeEstimates: GasFeeController["fetchGasFeeEstimates"];
|
|
94
|
+
}) {
|
|
95
|
+
super({ config, state, getCurrentChainId });
|
|
96
|
+
this.blockTracker = blockTracker;
|
|
97
|
+
this.getProviderConfig = getProviderConfig;
|
|
98
|
+
this._getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility;
|
|
99
|
+
this._getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility;
|
|
100
|
+
this.getSelectedAddress = getSelectedAddress;
|
|
101
|
+
this.getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates;
|
|
102
|
+
this.signEthTx = signEthTx;
|
|
103
|
+
this.provider = provider;
|
|
104
|
+
this.txGasUtil = new TransactionGasUtil(this.provider, this.blockTracker);
|
|
105
|
+
this.nonceTracker = new NonceTracker({
|
|
106
|
+
provider,
|
|
107
|
+
blockTracker,
|
|
108
|
+
getConfirmedTransactions: this.getConfirmedTransactions.bind(this),
|
|
109
|
+
getPendingTransactions: this.getSubmittedTransactions.bind(this), // nonce tracker should only care about submitted transactions
|
|
110
|
+
});
|
|
111
|
+
this.pendingTxTracker = new PendingTransactionTracker({
|
|
112
|
+
provider,
|
|
113
|
+
nonceTracker: this.nonceTracker,
|
|
114
|
+
getPendingTransactions: this.getPendingTransactions.bind(this), // pending tx tracker should only care about submitted and approved transactions
|
|
115
|
+
getConfirmedTransactions: this.getConfirmedTransactions.bind(this),
|
|
116
|
+
approveTransaction: this.approveTransaction.bind(this),
|
|
117
|
+
publishTransaction: (rawTx) => this.provider.request<[string], string>({ method: METHOD_TYPES.ETH_SEND_RAW_TRANSACTION, params: [rawTx] }),
|
|
118
|
+
});
|
|
119
|
+
this._setupListeners();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
addTransactionUnapproved(txMeta: EthereumTransactionMeta) {
|
|
123
|
+
this.addTransactionToState(txMeta);
|
|
124
|
+
this.emit(`${txMeta.id}:unapproved`, txMeta);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async addNewUnapprovedTransaction(
|
|
128
|
+
txParams: TransactionParams,
|
|
129
|
+
req: JRPCRequest<TransactionParams> & UserRequestApprovalParams & { origin: string }
|
|
130
|
+
): Promise<string> {
|
|
131
|
+
const txMeta = await this.createTransaction(txParams, req);
|
|
132
|
+
return this.processApproval(txMeta);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async processApproval(txMeta: EthereumTransactionMeta): Promise<string> {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const handleFinished = (msg: EthereumTransactionMeta) => {
|
|
138
|
+
if (msg.status === TransactionStatus.rejected) {
|
|
139
|
+
return reject(providerErrors.userRejectedRequest(`Transaction Signature: User denied message signature`));
|
|
140
|
+
}
|
|
141
|
+
if (msg.status === TransactionStatus.failed) {
|
|
142
|
+
return reject(rpcErrors.internal(`Transaction Signature: failed to sign message ${msg.error}`));
|
|
143
|
+
}
|
|
144
|
+
if (msg.status === TransactionStatus.submitted) {
|
|
145
|
+
return resolve(msg.transactionHash);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return reject(rpcErrors.internal(`Transaction Signature: Unknown problem: ${JSON.stringify(txMeta.transaction)}`));
|
|
149
|
+
};
|
|
150
|
+
this.once(`${txMeta.id}:finished`, handleFinished);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async approveTransaction(transactionID: string): Promise<void> {
|
|
155
|
+
const txMeta = this.getTransaction(transactionID);
|
|
156
|
+
if (this.inProcessOfSigning.has(transactionID)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.inProcessOfSigning.add(transactionID);
|
|
160
|
+
let nonceLock: NonceLockRes;
|
|
161
|
+
try {
|
|
162
|
+
this.setTxStatusApproved(transactionID);
|
|
163
|
+
const fromAddress = txMeta.transaction.from;
|
|
164
|
+
const { customNonceValue } = txMeta.transaction;
|
|
165
|
+
const customNonceValueNumber = Number(customNonceValue);
|
|
166
|
+
nonceLock = await this.nonceTracker.getNonceLock(fromAddress);
|
|
167
|
+
// add nonce to txParams
|
|
168
|
+
// if txMeta has previousGasParams then it is a retry at same nonce with
|
|
169
|
+
// higher gas settings and therefor the nonce should not be recalculated
|
|
170
|
+
const nonce = nonceLock.nextNonce;
|
|
171
|
+
const customOrNonce = customNonceValueNumber === 0 ? customNonceValue : customNonceValue || nonce;
|
|
172
|
+
txMeta.transaction.nonce = addHexPrefix(customOrNonce.toString(16));
|
|
173
|
+
// add nonce debugging information to txMeta
|
|
174
|
+
txMeta.nonceDetails = nonceLock.nonceDetails;
|
|
175
|
+
this.updateTransactionInState(txMeta, "transactions#approveTransaction");
|
|
176
|
+
// sign transaction
|
|
177
|
+
const rawTx = await this.signTransaction(transactionID);
|
|
178
|
+
await this.publishTransaction(transactionID, rawTx);
|
|
179
|
+
nonceLock.releaseLock();
|
|
180
|
+
} catch (err) {
|
|
181
|
+
try {
|
|
182
|
+
this.setTxStatusFailed(transactionID, err as Error);
|
|
183
|
+
} catch (err2) {
|
|
184
|
+
log.error(err2);
|
|
185
|
+
}
|
|
186
|
+
// must set transaction to submitted/failed before releasing lock
|
|
187
|
+
if (nonceLock) {
|
|
188
|
+
nonceLock.releaseLock();
|
|
189
|
+
}
|
|
190
|
+
// continue with error chain
|
|
191
|
+
throw err;
|
|
192
|
+
} finally {
|
|
193
|
+
this.inProcessOfSigning.delete(transactionID);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async signTransaction(txId: string): Promise<string> {
|
|
198
|
+
const txMeta = this.getTransaction(txId);
|
|
199
|
+
const chainId = this.getCurrentChainId();
|
|
200
|
+
const type = isEIP1559Transaction(txMeta) ? TRANSACTION_ENVELOPE_TYPES.FEE_MARKET : TRANSACTION_ENVELOPE_TYPES.LEGACY;
|
|
201
|
+
const txParams: TransactionParams = {
|
|
202
|
+
...txMeta.transaction,
|
|
203
|
+
type,
|
|
204
|
+
chainId,
|
|
205
|
+
gasLimit: txMeta.transaction.gas,
|
|
206
|
+
};
|
|
207
|
+
const fromAddress = txParams.from;
|
|
208
|
+
const common = await this.getCommonConfiguration(fromAddress);
|
|
209
|
+
const unsignedEthTx = TransactionFactory.fromTxData(txParams, { common });
|
|
210
|
+
const signedEthTx = await this.signEthTx<TypedTransaction, TypedTransaction>(unsignedEthTx, fromAddress);
|
|
211
|
+
txMeta.r = addHexPrefix(signedEthTx.r.toString(16));
|
|
212
|
+
txMeta.s = addHexPrefix(signedEthTx.s.toString(16));
|
|
213
|
+
txMeta.v = addHexPrefix(signedEthTx.v.toString(16));
|
|
214
|
+
this.updateTransactionInState(txMeta, "transactions#signTransaction: add r, s, v values");
|
|
215
|
+
this.setTxStatusSigned(txId);
|
|
216
|
+
const rawTx = addHexPrefix(Buffer.from(signedEthTx.serialize()).toString("hex"));
|
|
217
|
+
return rawTx;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async publishTransaction(txId: string, rawTx: string): Promise<void> {
|
|
221
|
+
const txMeta = this.getTransaction(txId);
|
|
222
|
+
txMeta.rawTransaction = rawTx;
|
|
223
|
+
this.updateTransactionInState(txMeta, "transactions#publishTransaction");
|
|
224
|
+
let txHash: string;
|
|
225
|
+
try {
|
|
226
|
+
txHash = await this.provider.request<[string], string>({ method: METHOD_TYPES.ETH_SEND_RAW_TRANSACTION, params: [rawTx] });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if ((error as Error).message.toLowerCase().includes("known transaction")) {
|
|
229
|
+
txHash = keccak256(addHexPrefix(rawTx));
|
|
230
|
+
txHash = addHexPrefix(txHash);
|
|
231
|
+
} else {
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.setTxHash(txId, txHash);
|
|
236
|
+
this.setTxStatusSubmitted(txId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async confirmTransaction(params: TX_CONFIRMED_EVENT_TYPE): Promise<void> {
|
|
240
|
+
const { txId, txReceipt } = params as TX_CONFIRMED_EVENT_TYPE & {
|
|
241
|
+
baseFeePerGas?: string;
|
|
242
|
+
blockTimestamp?: string;
|
|
243
|
+
txReceipt: TransactionReceipt;
|
|
244
|
+
};
|
|
245
|
+
log.info(params, "confirm params");
|
|
246
|
+
const txMeta = this.getTransaction(txId);
|
|
247
|
+
if (!txMeta) return;
|
|
248
|
+
try {
|
|
249
|
+
txMeta.txReceipt = {
|
|
250
|
+
...txReceipt,
|
|
251
|
+
};
|
|
252
|
+
this.setTxStatusConfirmed(txId);
|
|
253
|
+
this.markNonceDuplicatesDropped(txId);
|
|
254
|
+
this.updateTransactionInState(txMeta, "transactions#confirmTransaction - add txReceipt");
|
|
255
|
+
} catch (error) {
|
|
256
|
+
log.error(error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
cancelTransaction?(transactionID: string): Promise<void> {
|
|
261
|
+
throw new Error(`Method not implemented. ${transactionID}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async getEIP1559Compatibility(fromAddress?: string) {
|
|
265
|
+
const currentNetworkIsCompatible = await this._getCurrentNetworkEIP1559Compatibility();
|
|
266
|
+
const fromAccountIsCompatible = await this._getCurrentAccountEIP1559Compatibility(fromAddress);
|
|
267
|
+
return currentNetworkIsCompatible && fromAccountIsCompatible;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async addTransactionGasDefaults(txMeta: EthereumTransactionMeta) {
|
|
271
|
+
let updateTxMeta = txMeta;
|
|
272
|
+
try {
|
|
273
|
+
updateTxMeta = await this.addTxGasDefaults(txMeta);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
log.warn(error);
|
|
276
|
+
updateTxMeta = this.getTransaction(txMeta.id);
|
|
277
|
+
updateTxMeta.loadingDefaults = false;
|
|
278
|
+
this.updateTransactionInState(txMeta, "Failed to calculate gas defaults.");
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
updateTxMeta.loadingDefaults = false;
|
|
282
|
+
|
|
283
|
+
this.updateTransactionInState(updateTxMeta, "Added new unapproved transaction.");
|
|
284
|
+
|
|
285
|
+
return updateTxMeta;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async addTxGasDefaults(txMeta: EthereumTransactionMeta) {
|
|
289
|
+
const eip1559Compatibility = txMeta.transaction.type !== TRANSACTION_ENVELOPE_TYPES.LEGACY && (await this.getEIP1559Compatibility());
|
|
290
|
+
|
|
291
|
+
const {
|
|
292
|
+
gasPrice: defaultGasPrice,
|
|
293
|
+
maxFeePerGas: defaultMaxFeePerGas,
|
|
294
|
+
maxPriorityFeePerGas: defaultMaxPriorityFeePerGas,
|
|
295
|
+
} = await this.getDefaultGasFees(txMeta, eip1559Compatibility);
|
|
296
|
+
const { gasLimit: defaultGasLimit, simulationFails } = await this.getDefaultGasLimit(txMeta);
|
|
297
|
+
|
|
298
|
+
txMeta = this.getTransaction(txMeta.id);
|
|
299
|
+
if (simulationFails) {
|
|
300
|
+
txMeta.simulationFails = simulationFails;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (eip1559Compatibility) {
|
|
304
|
+
// If the dapp has suggested a gas price, but no maxFeePerGas or maxPriorityFeePerGas
|
|
305
|
+
// then we set maxFeePerGas and maxPriorityFeePerGas to the suggested gasPrice.
|
|
306
|
+
if (txMeta.transaction.gasPrice && !txMeta.transaction.maxFeePerGas && !txMeta.transaction.maxPriorityFeePerGas) {
|
|
307
|
+
txMeta.transaction.maxFeePerGas = txMeta.transaction.gasPrice;
|
|
308
|
+
// If the dapp has suggested a gas price, but no maxFeePerGas or maxPriorityFeePerGas
|
|
309
|
+
// then we set maxFeePerGas to the suggested gasPrice.
|
|
310
|
+
|
|
311
|
+
txMeta.transaction.maxPriorityFeePerGas = bnLessThan(
|
|
312
|
+
typeof defaultMaxPriorityFeePerGas === "string" ? stripHexPrefix(defaultMaxPriorityFeePerGas) : defaultMaxPriorityFeePerGas,
|
|
313
|
+
typeof txMeta.transaction.gasPrice === "string" ? stripHexPrefix(txMeta.transaction.gasPrice) : txMeta.transaction.gasPrice
|
|
314
|
+
)
|
|
315
|
+
? defaultMaxPriorityFeePerGas
|
|
316
|
+
: txMeta.transaction.gasPrice;
|
|
317
|
+
} else {
|
|
318
|
+
if (defaultMaxFeePerGas && !txMeta.transaction.maxFeePerGas) {
|
|
319
|
+
// If the dapp has not set the gasPrice or the maxFeePerGas, then we set maxFeePerGas
|
|
320
|
+
// with the one returned by the gasFeeController, if that is available.
|
|
321
|
+
txMeta.transaction.maxFeePerGas = defaultMaxFeePerGas;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (defaultMaxPriorityFeePerGas && !txMeta.transaction.maxPriorityFeePerGas) {
|
|
325
|
+
// If the dapp has not set the gasPrice or the maxPriorityFeePerGas, then we set maxPriorityFeePerGas
|
|
326
|
+
// with the one returned by the gasFeeController, if that is available.
|
|
327
|
+
txMeta.transaction.maxPriorityFeePerGas = defaultMaxPriorityFeePerGas;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (defaultGasPrice && !txMeta.transaction.maxFeePerGas) {
|
|
331
|
+
// If the dapp has not set the gasPrice or the maxFeePerGas, and no maxFeePerGas is available
|
|
332
|
+
// from the gasFeeController, then we set maxFeePerGas to the defaultGasPrice, assuming it is
|
|
333
|
+
// available.
|
|
334
|
+
txMeta.transaction.maxFeePerGas = defaultGasPrice;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (txMeta.transaction.maxFeePerGas && !txMeta.transaction.maxPriorityFeePerGas) {
|
|
338
|
+
// If the dapp has not set the gasPrice or the maxPriorityFeePerGas, and no maxPriorityFeePerGas is
|
|
339
|
+
// available from the gasFeeController, then we set maxPriorityFeePerGas to
|
|
340
|
+
// txMeta.transaction.maxFeePerGas, which will either be the gasPrice from the controller, the maxFeePerGas
|
|
341
|
+
// set by the dapp, or the maxFeePerGas from the controller.
|
|
342
|
+
txMeta.transaction.maxPriorityFeePerGas = txMeta.transaction.maxFeePerGas;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// We remove the gasPrice param entirely when on an eip1559 compatible network
|
|
347
|
+
|
|
348
|
+
delete txMeta.transaction.gasPrice;
|
|
349
|
+
} else {
|
|
350
|
+
// We ensure that maxFeePerGas and maxPriorityFeePerGas are not in the transaction params
|
|
351
|
+
// when not on a EIP1559 compatible network
|
|
352
|
+
|
|
353
|
+
delete txMeta.transaction.maxPriorityFeePerGas;
|
|
354
|
+
delete txMeta.transaction.maxFeePerGas;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// If we have gotten to this point, and none of gasPrice, maxPriorityFeePerGas or maxFeePerGas are
|
|
358
|
+
// set on transaction, it means that either we are on a non-EIP1559 network and the dapp didn't suggest
|
|
359
|
+
// a gas price, or we are on an EIP1559 network, and none of gasPrice, maxPriorityFeePerGas or maxFeePerGas
|
|
360
|
+
// were available from either the dapp or the network.
|
|
361
|
+
if (defaultGasPrice && !txMeta.transaction.gasPrice && !txMeta.transaction.maxPriorityFeePerGas && !txMeta.transaction.maxFeePerGas) {
|
|
362
|
+
txMeta.transaction.gasPrice = defaultGasPrice;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (defaultGasLimit && !txMeta.transaction.gas) {
|
|
366
|
+
txMeta.transaction.gas = defaultGasLimit;
|
|
367
|
+
}
|
|
368
|
+
return txMeta;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
setTxHash(txId: string, txHash: string) {
|
|
372
|
+
// Add the tx hash to the persisted meta-tx object
|
|
373
|
+
const txMeta = this.getTransaction(txId);
|
|
374
|
+
txMeta.transactionHash = txHash;
|
|
375
|
+
this.updateTransactionInState(txMeta, "transactions#setTxHash");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getUnapprovedTxCount = () => Object.keys(this.getUnapprovedTxList()).length;
|
|
379
|
+
|
|
380
|
+
getPendingTxCount = (account?: string) => this.getPendingTransactions(account).length;
|
|
381
|
+
|
|
382
|
+
async getDefaultGasFees(
|
|
383
|
+
txMeta: EthereumTransactionMeta,
|
|
384
|
+
eip1559Compatibility: boolean
|
|
385
|
+
): Promise<{ maxFeePerGas?: string; maxPriorityFeePerGas?: string; gasPrice?: string }> {
|
|
386
|
+
if (
|
|
387
|
+
(!eip1559Compatibility && txMeta.transaction.gasPrice) ||
|
|
388
|
+
(eip1559Compatibility && txMeta.transaction.maxFeePerGas && txMeta.transaction.maxPriorityFeePerGas)
|
|
389
|
+
) {
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const { gasFeeEstimates, gasEstimateType } = await this.getEIP1559GasFeeEstimates();
|
|
395
|
+
if (eip1559Compatibility && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
|
|
396
|
+
// this is in dec gwei
|
|
397
|
+
const { medium: { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = {} } = <EthereumGasFeeEstimates>gasFeeEstimates;
|
|
398
|
+
|
|
399
|
+
if (suggestedMaxPriorityFeePerGas && suggestedMaxFeePerGas) {
|
|
400
|
+
return {
|
|
401
|
+
// send to controller in hex wei
|
|
402
|
+
maxFeePerGas: addHexPrefix(decGWEIToHexWEI(new BigNumber(suggestedMaxFeePerGas)).toString(16)),
|
|
403
|
+
maxPriorityFeePerGas: addHexPrefix(decGWEIToHexWEI(new BigNumber(suggestedMaxPriorityFeePerGas)).toString(16)),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
|
|
407
|
+
const { medium } = <EthereumLegacyGasFeeEstimates>gasFeeEstimates;
|
|
408
|
+
// The LEGACY type includes low, medium and high estimates of
|
|
409
|
+
// gas price values.
|
|
410
|
+
return {
|
|
411
|
+
gasPrice: addHexPrefix(decGWEIToHexWEI(new BigNumber(medium)).toString(16)),
|
|
412
|
+
};
|
|
413
|
+
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
|
|
414
|
+
const { gasPrice } = <{ gasPrice?: string }>gasFeeEstimates;
|
|
415
|
+
// The ETH_GASPRICE type just includes a single gas price property,
|
|
416
|
+
// which we can assume was retrieved from eth_gasPrice
|
|
417
|
+
return {
|
|
418
|
+
gasPrice: addHexPrefix(decGWEIToHexWEI(new BigNumber(gasPrice)).toString(16)),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
log.error(error);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const gasPrice = await this.provider.request<never, string>({ method: METHOD_TYPES.ETH_GET_GAS_PRICE });
|
|
426
|
+
|
|
427
|
+
return { gasPrice: gasPrice && addHexPrefix(gasPrice) };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private async getDefaultGasLimit(txMeta: EthereumTransactionMeta) {
|
|
431
|
+
const chainId = this.getCurrentChainId();
|
|
432
|
+
const customNetworkGasBuffer = CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId];
|
|
433
|
+
const chainType = getChainType(chainId);
|
|
434
|
+
|
|
435
|
+
if (txMeta.transaction.gas) {
|
|
436
|
+
return {};
|
|
437
|
+
}
|
|
438
|
+
if (txMeta.transaction.to && txMeta.transactionCategory === TRANSACTION_TYPES.SENT_ETHER && chainType !== "custom" && !txMeta.transaction.data) {
|
|
439
|
+
// This is a standard ether simple send, gas requirement is exactly 21k
|
|
440
|
+
return { gasLimit: GAS_LIMITS.SIMPLE };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const { blockGasLimit, estimatedGasHex, simulationFails } = await this.txGasUtil.analyzeGasUsage(txMeta);
|
|
444
|
+
|
|
445
|
+
// add additional gas buffer to our estimation for safety
|
|
446
|
+
const gasLimit = this.txGasUtil.addGasBuffer(addHexPrefix(estimatedGasHex), blockGasLimit, customNetworkGasBuffer);
|
|
447
|
+
return { gasLimit, simulationFails };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private async createTransaction(
|
|
451
|
+
txParameters: TransactionParams,
|
|
452
|
+
req: JRPCRequest<TransactionParams> & UserRequestApprovalParams
|
|
453
|
+
): Promise<EthereumTransactionMeta> {
|
|
454
|
+
const normalizedTxParameters = normalizeTxParameters(txParameters);
|
|
455
|
+
const eip1559Compatibility = await this.getEIP1559Compatibility(txParameters.from);
|
|
456
|
+
validateTxParameters(normalizedTxParameters, eip1559Compatibility);
|
|
457
|
+
|
|
458
|
+
let txMeta = this.generateTxMeta({
|
|
459
|
+
transaction: normalizedTxParameters,
|
|
460
|
+
origin: req.origin,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const { type, category, methodParams } = await determineTransactionType(txParameters, this.provider);
|
|
464
|
+
txMeta.type = type;
|
|
465
|
+
txMeta.transactionCategory = category;
|
|
466
|
+
txMeta.methodParams = methodParams;
|
|
467
|
+
txMeta.transaction.value = txMeta.transaction.value ? addHexPrefix(txMeta.transaction.value) : "0x0";
|
|
468
|
+
this.emit(`${txMeta.id}:unapproved`, txMeta);
|
|
469
|
+
txMeta = this.addTransactionToState(txMeta);
|
|
470
|
+
txMeta = await this.addTransactionGasDefaults(txMeta);
|
|
471
|
+
|
|
472
|
+
this.emit(TX_EVENTS.TX_UNAPPROVED, { txMeta, req });
|
|
473
|
+
|
|
474
|
+
return txMeta;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private _setupListeners() {
|
|
478
|
+
this.setupBlockTrackerListener();
|
|
479
|
+
this.pendingTxTracker.on(TX_EVENTS.TX_WARNING, (data: TX_WARNING_EVENT_TYPE<TransactionParams, EthereumTransactionMeta>) => {
|
|
480
|
+
this.updateTransactionInState(data.txMeta);
|
|
481
|
+
});
|
|
482
|
+
this.pendingTxTracker.on(TX_EVENTS.TX_DROPPED, (data: TX_DROPPED_EVENT_TYPE) => this.setTxStatusDropped(data.txId));
|
|
483
|
+
this.pendingTxTracker.on(
|
|
484
|
+
TX_EVENTS.TX_BLOCK_UPDATE,
|
|
485
|
+
({ txMeta, latestBlockNumber }: { txMeta: EthereumTransactionMeta; latestBlockNumber: string; txId: string }) => {
|
|
486
|
+
if (!txMeta.firstRetryBlockNumber) {
|
|
487
|
+
txMeta.firstRetryBlockNumber = latestBlockNumber;
|
|
488
|
+
this.updateTransactionInState(txMeta);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
this.pendingTxTracker.on(TX_EVENTS.TX_RETRY, (txMeta) => {
|
|
493
|
+
if (!("retryCount" in txMeta)) {
|
|
494
|
+
txMeta.retryCount = 0;
|
|
495
|
+
}
|
|
496
|
+
txMeta.retryCount += 1;
|
|
497
|
+
this.updateTransactionInState(txMeta);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
this.pendingTxTracker.on(TX_EVENTS.TX_FAILED, (data: TX_FAILED_EVENT_TYPE) => {
|
|
501
|
+
this.setTxStatusFailed(data.txId, data.error);
|
|
502
|
+
});
|
|
503
|
+
this.pendingTxTracker.on(TX_EVENTS.TX_CONFIRMED, (data: TX_CONFIRMED_EVENT_TYPE) => this.confirmTransaction(data));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private setupBlockTrackerListener() {
|
|
507
|
+
let listenersAreActive = false;
|
|
508
|
+
const latestBlockHandler = this.onLatestBlock.bind(this);
|
|
509
|
+
this.on(TX_EVENTS.TX_STATUS_UPDATE, () => {
|
|
510
|
+
const pendingTxs = this.getPendingTransactions();
|
|
511
|
+
if (!listenersAreActive && pendingTxs.length > 0) {
|
|
512
|
+
this.blockTracker.on("latest", latestBlockHandler);
|
|
513
|
+
listenersAreActive = true;
|
|
514
|
+
} else if (listenersAreActive && !pendingTxs.length) {
|
|
515
|
+
this.blockTracker.removeListener("latest", latestBlockHandler);
|
|
516
|
+
listenersAreActive = false;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private async onLatestBlock(blockNumber: EthereumBlock) {
|
|
522
|
+
try {
|
|
523
|
+
await this.pendingTxTracker.updatePendingTxs();
|
|
524
|
+
} catch (error) {
|
|
525
|
+
log.error(error);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
await this.pendingTxTracker.resubmitPendingTxs(blockNumber);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
log.error(error);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private async getCommonConfiguration(fromAddress: string) {
|
|
536
|
+
const { chainId, displayName } = this.getProviderConfig();
|
|
537
|
+
const supportsEIP1559 = await this.getEIP1559Compatibility(fromAddress);
|
|
538
|
+
const hardfork = supportsEIP1559 ? Hardfork.Paris : Hardfork.Berlin;
|
|
539
|
+
return Common.custom({
|
|
540
|
+
chainId: chainId === "loading" ? 0 : Number.parseInt(chainId, 16),
|
|
541
|
+
defaultHardfork: hardfork,
|
|
542
|
+
name: displayName,
|
|
543
|
+
networkId: chainId === "loading" ? 0 : Number.parseInt(chainId, 16),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private markNonceDuplicatesDropped(txId: string) {
|
|
548
|
+
const txMeta = this.getTransaction(txId);
|
|
549
|
+
const { nonce, from } = txMeta.transaction;
|
|
550
|
+
const sameNonceTxs = this.getTransactions({ searchCriteria: { from, nonce } });
|
|
551
|
+
if (!sameNonceTxs.length) return;
|
|
552
|
+
sameNonceTxs.forEach((tx) => {
|
|
553
|
+
if (tx.id === txId) return;
|
|
554
|
+
this.updateTransactionInState(txMeta, "transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce");
|
|
555
|
+
if (tx.status !== TransactionStatus.failed) this.setTxStatusDropped(tx.id);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { addHexPrefix, stripHexPrefix } from "@ethereumjs/util";
|
|
2
|
+
import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
|
|
3
|
+
import { BN } from "bn.js";
|
|
4
|
+
import { cloneDeep } from "lodash";
|
|
5
|
+
import log from "loglevel";
|
|
6
|
+
|
|
7
|
+
import PollingBlockTracker from "../Block/PollingBlockTracker";
|
|
8
|
+
import { EthereumTransactionMeta, TransactionParams } from "../utils/interfaces";
|
|
9
|
+
export default class TransactionGasUtil {
|
|
10
|
+
provider: SafeEventEmitterProvider;
|
|
11
|
+
|
|
12
|
+
blockTracker: PollingBlockTracker;
|
|
13
|
+
|
|
14
|
+
constructor(provider: SafeEventEmitterProvider, blockTracker: PollingBlockTracker) {
|
|
15
|
+
this.provider = provider;
|
|
16
|
+
this.blockTracker = blockTracker;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async analyzeGasUsage(txMeta: EthereumTransactionMeta) {
|
|
20
|
+
const block = await this.blockTracker.getLatestBlock();
|
|
21
|
+
// fallback to block gasLimit
|
|
22
|
+
const blockGasLimitBN = new BN(stripHexPrefix(block.gasLimit), 16);
|
|
23
|
+
const saferGasLimitBN = blockGasLimitBN.mul(new BN(19)).div(new BN(20));
|
|
24
|
+
let estimatedGasHex = addHexPrefix(saferGasLimitBN.toString("hex"));
|
|
25
|
+
let simulationFails: Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
estimatedGasHex = await this.estimateTxGas(txMeta);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
log.warn(error);
|
|
31
|
+
simulationFails = {
|
|
32
|
+
reason: (error as Error).message,
|
|
33
|
+
errorKey: (error as Error & { errorKey: string }).errorKey,
|
|
34
|
+
debug: { blockNumber: block.idempotencyKey, blockGasLimit: block.gasLimit },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return { blockGasLimit: block.gasLimit, estimatedGasHex, simulationFails };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
Adds a gas buffer with out exceeding the block gas limit
|
|
42
|
+
*/
|
|
43
|
+
public addGasBuffer(initialGasLimitHex: string, blockGasLimitHex: string, multiplier = 1.5): string {
|
|
44
|
+
const initialGasLimitBn = new BN(stripHexPrefix(initialGasLimitHex), 16);
|
|
45
|
+
const blockGasLimitBn = new BN(stripHexPrefix(blockGasLimitHex), 16);
|
|
46
|
+
const upperGasLimitBn = blockGasLimitBn.muln(0.9);
|
|
47
|
+
const bufferedGasLimitBn = initialGasLimitBn.muln(multiplier);
|
|
48
|
+
|
|
49
|
+
// if initialGasLimit is above blockGasLimit, dont modify it
|
|
50
|
+
if (initialGasLimitBn.gt(upperGasLimitBn)) return addHexPrefix(initialGasLimitBn.toString("hex"));
|
|
51
|
+
// if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
|
|
52
|
+
if (bufferedGasLimitBn.lt(upperGasLimitBn)) return addHexPrefix(bufferedGasLimitBn.toString("hex"));
|
|
53
|
+
// otherwise use blockGasLimit
|
|
54
|
+
return addHexPrefix(upperGasLimitBn.toString("hex"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
Estimates the tx's gas usage
|
|
59
|
+
*/
|
|
60
|
+
private async estimateTxGas(txMeta: EthereumTransactionMeta): Promise<string> {
|
|
61
|
+
const txParams = cloneDeep(txMeta.transaction);
|
|
62
|
+
|
|
63
|
+
// `eth_estimateGas` can fail if the user has insufficient balance for the
|
|
64
|
+
// value being sent, or for the gas cost. We don't want to check their
|
|
65
|
+
// balance here, we just want the gas estimate. The gas price is removed
|
|
66
|
+
// to skip those balance checks. We check balance elsewhere. We also delete
|
|
67
|
+
// maxFeePerGas and maxPriorityFeePerGas to support EIP-1559 txs.
|
|
68
|
+
delete txParams.gasPrice;
|
|
69
|
+
delete txParams.maxFeePerGas;
|
|
70
|
+
delete txParams.maxPriorityFeePerGas;
|
|
71
|
+
|
|
72
|
+
return this.provider.request<[TransactionParams], string>({ method: "eth_estimateGas", params: [txParams] });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import jsonDiffer, { Operation } from "fast-json-patch";
|
|
2
|
+
import { cloneDeep } from "lodash";
|
|
3
|
+
|
|
4
|
+
import { EthereumTransactionMeta } from "../utils/interfaces";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
Generates an array of history objects sense the previous state.
|
|
8
|
+
The object has the keys
|
|
9
|
+
op (the operation performed),
|
|
10
|
+
path (the key and if a nested object then each key will be seperated with a `/`)
|
|
11
|
+
value
|
|
12
|
+
with the first entry having the note and a timestamp when the change took place
|
|
13
|
+
*/
|
|
14
|
+
function generateHistoryEntry(previousState: Record<string, unknown>, newState: Record<string, unknown>, note?: string): Record<string, unknown>[] {
|
|
15
|
+
const entry = jsonDiffer.compare(previousState, newState) as unknown as Record<string, unknown>[];
|
|
16
|
+
// Add a note to the first op, since it breaks if we append it to the entry
|
|
17
|
+
if (entry[0]) {
|
|
18
|
+
if (note) {
|
|
19
|
+
entry[0].note = note;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
entry[0].timestamp = Date.now();
|
|
23
|
+
}
|
|
24
|
+
return entry;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
Recovers previous txMeta state obj
|
|
29
|
+
*/
|
|
30
|
+
function replayHistory(_shortHistory: Record<string, unknown>[]): Record<string, unknown> {
|
|
31
|
+
const shortHistory = cloneDeep(_shortHistory);
|
|
32
|
+
return shortHistory.reduce((val: Record<string, unknown>, entry: unknown) => jsonDiffer.applyPatch(val, entry as Operation[]).newDocument);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function snapshotFromTxMeta(txMeta: EthereumTransactionMeta): EthereumTransactionMeta {
|
|
36
|
+
const shallow = { ...txMeta };
|
|
37
|
+
delete shallow.history;
|
|
38
|
+
return cloneDeep(shallow);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { generateHistoryEntry, replayHistory, snapshotFromTxMeta };
|