@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,315 @@
1
+ import {
2
+ BaseTransactionStateManager,
3
+ ITransactionStateManager,
4
+ randomId,
5
+ TransactionConfig,
6
+ transactionMatchesNetwork,
7
+ TransactionState,
8
+ TransactionStatus,
9
+ TX_EVENTS,
10
+ TX_STATUS_UPDATE_EVENT_TYPE,
11
+ } from "@toruslabs/base-controllers";
12
+ import { keyBy, mapValues, omitBy, pickBy, sortBy } from "lodash";
13
+
14
+ import NetworkController from "../Network/NetworkController";
15
+ import { DappSuggestedGasFees, EthereumTransactionMeta, TransactionParams } from "../utils/interfaces";
16
+ import { generateHistoryEntry, replayHistory, snapshotFromTxMeta } from "./TransactionStateHistoryHelper";
17
+ import { getFinalStates, normalizeAndValidateTxParams } from "./TransactionUtils";
18
+
19
+ export default class TransactionStateManager
20
+ extends BaseTransactionStateManager<TransactionParams, EthereumTransactionMeta>
21
+ implements ITransactionStateManager<TransactionParams>
22
+ {
23
+ constructor({
24
+ config,
25
+ state,
26
+ getCurrentChainId,
27
+ }: {
28
+ config?: Partial<TransactionConfig>;
29
+ state?: Partial<TransactionState<TransactionParams, EthereumTransactionMeta>>;
30
+ getCurrentChainId: NetworkController["getNetworkIdentifier"];
31
+ }) {
32
+ super({ config, state, getCurrentChainId });
33
+ }
34
+
35
+ generateTxMeta(opts: Partial<EthereumTransactionMeta> = {}): EthereumTransactionMeta {
36
+ const chainId = this.getCurrentChainId();
37
+ if (chainId === "loading") throw new Error("Torus is having trouble connecting to the network");
38
+ let dappSuggestedGasFees: DappSuggestedGasFees = null;
39
+
40
+ // If we are dealing with a transaction suggested by a dapp and not
41
+ // an internally created transaction, we need to keep record of
42
+ // the originally submitted gasParams.
43
+ if (opts.transaction && typeof opts.origin === "string" && opts.origin !== "torus") {
44
+ if (typeof opts.transaction.gasPrice !== "undefined") {
45
+ dappSuggestedGasFees = {
46
+ gasPrice: opts.transaction.gasPrice,
47
+ };
48
+ } else if (typeof opts.transaction.maxFeePerGas !== "undefined" || typeof opts.transaction.maxPriorityFeePerGas !== "undefined") {
49
+ dappSuggestedGasFees = {
50
+ maxPriorityFeePerGas: opts.transaction.maxPriorityFeePerGas,
51
+ maxFeePerGas: opts.transaction.maxFeePerGas,
52
+ };
53
+ }
54
+
55
+ if (typeof opts.transaction.gas !== "undefined") {
56
+ dappSuggestedGasFees = {
57
+ ...dappSuggestedGasFees,
58
+ gas: opts.transaction.gas,
59
+ };
60
+ }
61
+ }
62
+
63
+ return {
64
+ id: randomId(),
65
+ time: Date.now(),
66
+ status: TransactionStatus.unapproved,
67
+ loadingDefaults: true,
68
+ chainId,
69
+ dappSuggestedGasFees,
70
+ ...opts,
71
+ } as EthereumTransactionMeta;
72
+ }
73
+
74
+ addTransactionToState(txMeta: EthereumTransactionMeta): EthereumTransactionMeta {
75
+ if (txMeta.transaction) {
76
+ txMeta.transaction = normalizeAndValidateTxParams(txMeta.transaction, false);
77
+ }
78
+ this.once(`${txMeta.id}:signed`, () => {
79
+ this.removeAllListeners(`${txMeta.id}:rejected`);
80
+ });
81
+ this.once(`${txMeta.id}:rejected`, () => {
82
+ this.removeAllListeners(`${txMeta.id}:signed`);
83
+ });
84
+ // initialize history
85
+ txMeta.history = [];
86
+ // capture initial snapshot of txMeta for history
87
+ const snapshot = snapshotFromTxMeta(txMeta);
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ txMeta.history.push(snapshot as any);
90
+
91
+ const transactions = this.getTransactions({
92
+ filterToCurrentNetwork: false,
93
+ });
94
+ const { txHistoryLimit } = this.config;
95
+
96
+ // checks if the length of the tx history is longer then desired persistence
97
+ // limit and then if it is removes the oldest confirmed or rejected tx.
98
+ // Pending or unapproved transactions will not be removed by this
99
+ // operation. For safety of presenting a fully functional transaction UI
100
+ // representation, this function will not break apart transactions with the
101
+ // same nonce, per network. Not accounting for transactions of the same
102
+ // nonce and network combo can result in confusing or broken experiences
103
+ // in the UI.
104
+ //
105
+ // we will send UI only collected groups of transactions *per page* so at
106
+ // some point in the future, this persistence limit can be adjusted. When
107
+ // we do that I think we should figure out a better storage solution for
108
+ // transaction history entries.
109
+ const nonceNetworkSet = new Set();
110
+
111
+ const txsToDelete = transactions
112
+ .reverse()
113
+ .filter((tx) => {
114
+ const { nonce } = tx.transaction;
115
+ const { chainId, status } = tx;
116
+ const key = `${nonce}-${chainId}`;
117
+ if (nonceNetworkSet.has(key)) {
118
+ return false;
119
+ }
120
+ if (nonceNetworkSet.size < txHistoryLimit - 1 || getFinalStates().includes(status) === false) {
121
+ nonceNetworkSet.add(key);
122
+ return false;
123
+ }
124
+ return true;
125
+ })
126
+ .map((tx) => tx.id);
127
+
128
+ this._deleteTransactions(txsToDelete);
129
+ this._addTransactionsToState([txMeta]);
130
+ return txMeta;
131
+ }
132
+
133
+ /**
134
+ Removes transaction from the given address for the current network
135
+ from the txList
136
+ */
137
+ wipeTransactions(address: string): void {
138
+ const { transactions } = this.state;
139
+ const chainId = this.getCurrentChainId();
140
+
141
+ this.update({
142
+ transactions: omitBy(transactions, (txMeta: EthereumTransactionMeta) => {
143
+ const transactionMatch = transactionMatchesNetwork(txMeta, chainId);
144
+ return txMeta.transaction.from === address && transactionMatch;
145
+ }),
146
+ });
147
+ }
148
+
149
+ getTransactions({
150
+ searchCriteria = {},
151
+ initialList = undefined,
152
+ filterToCurrentNetwork = true,
153
+ limit = undefined,
154
+ }: {
155
+ searchCriteria?: Record<string, (val: unknown) => boolean> | Record<string, unknown>;
156
+ initialList?: EthereumTransactionMeta[];
157
+ filterToCurrentNetwork?: boolean;
158
+ limit?: number;
159
+ } = {}): EthereumTransactionMeta[] {
160
+ const chainId = this.getCurrentChainId();
161
+ // searchCriteria is an object that might have values that aren't predicate
162
+ // methods. When providing any other value type (string, number, etc), we
163
+ // consider this shorthand for "check the value at key for strict equality
164
+ // with the provided value". To conform this object to be only methods, we
165
+ // mapValues (lodash) such that every value on the object is a method that
166
+ // returns a boolean.
167
+ const predicateMethods: unknown = mapValues(searchCriteria, (predicate) =>
168
+ typeof predicate === "function" ? predicate : (v: unknown) => v === predicate
169
+ );
170
+
171
+ // If an initial list is provided we need to change it back into an object
172
+ // first, so that it matches the shape of our state. This is done by the
173
+ // lodash keyBy method. This is the edge case for this method, typically
174
+ // initialList will be undefined.
175
+ const transactionsToFilter = initialList ? keyBy(initialList, "id") : this.state.transactions;
176
+
177
+ // Combine sortBy and pickBy to transform our state object into an array of
178
+ // matching transactions that are sorted by time.
179
+ const filteredTransactions = sortBy(
180
+ pickBy(transactionsToFilter, (txMeta) => {
181
+ // default matchesCriteria to the value of transactionMatchesNetwork
182
+ // when filterToCurrentNetwork is true.
183
+ const transactionMatches = transactionMatchesNetwork(txMeta, chainId);
184
+ if (filterToCurrentNetwork && !transactionMatches) {
185
+ return false;
186
+ }
187
+ // iterate over the predicateMethods keys to check if the transaction
188
+ // matches the searchCriteria
189
+ for (const [key, predicate] of Object.entries(predicateMethods)) {
190
+ // We return false early as soon as we know that one of the specified
191
+ // search criteria do not match the transaction. This prevents
192
+ // needlessly checking all criteria when we already know the criteria
193
+ // are not fully satisfied. We check both txParams and the base
194
+ // object as predicate keys can be either.
195
+ if (key in txMeta.transaction) {
196
+ if (predicate(txMeta.transaction[key as keyof TransactionParams]) === false) {
197
+ return false;
198
+ }
199
+ } else if (predicate(txMeta[key as keyof EthereumTransactionMeta]) === false) {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ return true;
205
+ }),
206
+ "time"
207
+ );
208
+
209
+ if (limit !== undefined) {
210
+ // We need to have all transactions of a given nonce in order to display
211
+ // necessary details in the UI. We use the size of this set to determine
212
+ // whether we have reached the limit provided, thus ensuring that all
213
+ // transactions of nonces we include will be sent to the UI.
214
+ const nonces = new Set();
215
+ const txs = [];
216
+ // By default, the transaction list we filter from is sorted by time ASC.
217
+ // To ensure that filtered results prefers the newest transactions we
218
+ // iterate from right to left, inserting transactions into front of a new
219
+ // array. The original order is preserved, but we ensure that newest txs
220
+ // are preferred.
221
+ for (let i = filteredTransactions.length - 1; i > -1; i -= 1) {
222
+ const txMeta = filteredTransactions[i];
223
+ const { nonce } = txMeta.transaction;
224
+ if (!nonces.has(nonce)) {
225
+ if (nonces.size < limit) {
226
+ nonces.add(nonce);
227
+ } else {
228
+ continue;
229
+ }
230
+ }
231
+ // Push transaction into the beginning of our array to ensure the
232
+ // original order is preserved.
233
+ txs.unshift(txMeta);
234
+ }
235
+ return txs;
236
+ }
237
+
238
+ return filteredTransactions;
239
+ }
240
+
241
+ getApprovedTransactions(address?: string): EthereumTransactionMeta[] {
242
+ const searchCriteria: { status: TransactionStatus; from?: string } = { status: TransactionStatus.approved };
243
+ if (address) {
244
+ searchCriteria.from = address;
245
+ }
246
+ return this.getTransactions({ searchCriteria });
247
+ }
248
+
249
+ getSubmittedTransactions(address?: string): EthereumTransactionMeta[] {
250
+ const searchCriteria: { status: TransactionStatus; from?: string } = { status: TransactionStatus.submitted };
251
+ if (address) {
252
+ searchCriteria.from = address;
253
+ }
254
+ return this.getTransactions({ searchCriteria });
255
+ }
256
+
257
+ getPendingTransactions(address?: string): EthereumTransactionMeta[] {
258
+ const submitted = this.getSubmittedTransactions(address);
259
+ const approved = this.getApprovedTransactions(address);
260
+ return [...submitted, ...approved];
261
+ }
262
+
263
+ getConfirmedTransactions(address?: string): EthereumTransactionMeta[] {
264
+ const searchCriteria: { status: TransactionStatus; from?: string } = { status: TransactionStatus.confirmed };
265
+ if (address) {
266
+ searchCriteria.from = address;
267
+ }
268
+ return this.getTransactions({ searchCriteria });
269
+ }
270
+
271
+ getUnapprovedTxList(): Record<string, EthereumTransactionMeta> {
272
+ const chainId = this.getCurrentChainId();
273
+
274
+ return pickBy(this.state.transactions, (transaction) => {
275
+ const transactionMatches = transactionMatchesNetwork(transaction, chainId);
276
+ return transaction.status === TransactionStatus.unapproved && transactionMatches;
277
+ });
278
+ }
279
+
280
+ updateTransactionInState(txMeta: EthereumTransactionMeta, note?: string) {
281
+ // validate txParams
282
+ if (txMeta.transaction) {
283
+ txMeta.transaction = normalizeAndValidateTxParams(txMeta.transaction, false);
284
+ }
285
+
286
+ // create txMeta snapshot for history
287
+ const currentState = snapshotFromTxMeta(txMeta);
288
+ // recover previous tx state obj
289
+ const previousState = replayHistory(txMeta.history);
290
+ // generate history entry and add to history
291
+ const entry = generateHistoryEntry(previousState, currentState as unknown as Record<string, unknown>, note);
292
+ if (entry.length > 0) {
293
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
+ txMeta.history.push(entry as any);
295
+ }
296
+
297
+ // commit txMeta to state
298
+ this.updateTransaction(txMeta);
299
+ }
300
+
301
+ protected override _setTransactionStatus(txId: string, status: TransactionStatus, isFinalStep?: boolean): void {
302
+ const txMeta = this.getTransaction(txId);
303
+ if (!txMeta) {
304
+ return;
305
+ }
306
+ txMeta.status = status;
307
+ this.updateTransactionInState(txMeta);
308
+ this.emit(TX_EVENTS.TX_STATUS_UPDATE, { txId, status } as TX_STATUS_UPDATE_EVENT_TYPE);
309
+ if (this.isFinalState(status) || isFinalStep) {
310
+ this.emit(`${txMeta.id}:finished`, txMeta);
311
+ } else {
312
+ this.emit(`${txMeta.id}:${status}`, txId);
313
+ }
314
+ }
315
+ }
@@ -0,0 +1,333 @@
1
+ import { addHexPrefix, isHexString, isValidAddress } from "@ethereumjs/util";
2
+ import { rpcErrors } from "@metamask/rpc-errors";
3
+ import { randomId, TRANSACTION_TYPE, TRANSACTION_TYPES, TransactionStatus } from "@toruslabs/base-controllers";
4
+ import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
5
+ import { Interface } from "ethers";
6
+ import log from "loglevel";
7
+
8
+ import { ecr20Abi, erc721Abi, erc1155Abi } from "../utils/abis";
9
+ import {
10
+ CONTRACT_TYPE_ERC20,
11
+ CONTRACT_TYPE_ERC721,
12
+ CONTRACT_TYPE_ERC1155,
13
+ CONTRACT_TYPE_ETH,
14
+ METHOD_TYPES,
15
+ TRANSACTION_ENVELOPE_TYPES,
16
+ } from "../utils/constants";
17
+ import { EthereumTransactionMeta, TRANSACTION_ENVELOPE_TYPES_TYPE, TransactionParams } from "../utils/interfaces";
18
+
19
+ const erc20Interface = new Interface(ecr20Abi);
20
+ const erc721Interface = new Interface(erc721Abi);
21
+ const erc1155Interface = new Interface(erc1155Abi);
22
+
23
+ // functions that handle normalizing of that key in txParams
24
+ type NormalizableTransactionParams = keyof Omit<TransactionParams, "accessList">;
25
+ const normalizers: Partial<
26
+ Record<
27
+ NormalizableTransactionParams,
28
+ (param: NormalizableTransactionParams, ...args: unknown[]) => TransactionParams[NormalizableTransactionParams]
29
+ >
30
+ > = {
31
+ from: (from: string, LowerCase = true) => (LowerCase ? addHexPrefix(from).toLowerCase() : addHexPrefix(from)),
32
+ to: (to: string, LowerCase = true) => (LowerCase ? addHexPrefix(to).toLowerCase() : addHexPrefix(to)),
33
+ nonce: (nonce: string) => addHexPrefix(nonce),
34
+ customNonceValue: (nonce: string) => addHexPrefix(nonce),
35
+ value: (value: string) => addHexPrefix(value),
36
+ data: (data: string) => addHexPrefix(data),
37
+ gas: (gas: string) => addHexPrefix(gas),
38
+ gasPrice: (gasPrice: string) => addHexPrefix(gasPrice),
39
+ type: addHexPrefix as (str: string) => TRANSACTION_ENVELOPE_TYPES_TYPE,
40
+ maxFeePerGas: addHexPrefix,
41
+ maxPriorityFeePerGas: addHexPrefix,
42
+ };
43
+
44
+ /**
45
+ * normalizes txParams
46
+ */
47
+ export function normalizeTxParameters(txParameters: TransactionParams, lowerCase = true): TransactionParams {
48
+ // apply only keys in the normalizers
49
+ const normalizedTxParameters: TransactionParams = { id: txParameters.id || randomId(), from: txParameters.from };
50
+ for (const key in normalizers) {
51
+ const currentKey = key as NormalizableTransactionParams;
52
+ if (txParameters[currentKey])
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ normalizedTxParameters[currentKey] = normalizers[currentKey](txParameters[currentKey] as NormalizableTransactionParams, lowerCase) as any;
55
+ }
56
+ return normalizedTxParameters;
57
+ }
58
+
59
+ export function transactionMatchesNetwork(transaction: EthereumTransactionMeta, chainId: string) {
60
+ if (typeof transaction.chainId !== "undefined") {
61
+ return transaction.chainId === chainId;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Determines if the maxFeePerGas and maxPriorityFeePerGas fields are supplied
68
+ * and valid inputs. This will return false for non hex string inputs.
69
+ * the transaction to check
70
+ * @returns true if transaction uses valid EIP1559 fields
71
+ */
72
+ export function isEIP1559Transaction(transaction: Partial<EthereumTransactionMeta>) {
73
+ return (
74
+ isHexString(addHexPrefix(transaction?.transaction?.maxFeePerGas)) && isHexString(addHexPrefix(transaction?.transaction?.maxPriorityFeePerGas))
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Determine if the maxFeePerGas and maxPriorityFeePerGas fields are not
80
+ * supplied and that the gasPrice field is valid if it is provided. This will
81
+ * return false if gasPrice is a non hex string.
82
+ * transaction -
83
+ * the transaction to check
84
+ * @returns true if transaction uses valid Legacy fields OR lacks
85
+ * EIP1559 fields
86
+ */
87
+ export function isLegacyTransaction(transaction: EthereumTransactionMeta) {
88
+ return (
89
+ typeof transaction.transaction.maxFeePerGas === "undefined" &&
90
+ typeof transaction.transaction.maxPriorityFeePerGas === "undefined" &&
91
+ (typeof transaction.transaction.gasPrice === "undefined" || isHexString(addHexPrefix(transaction.transaction.gasPrice)))
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Given two fields, ensure that the second field is not included in txParams,
97
+ * and if it is throw an invalidParams error.
98
+ */
99
+ export function ensureMutuallyExclusiveFieldsNotProvided(
100
+ txParams: TransactionParams,
101
+ fieldBeingValidated: NormalizableTransactionParams,
102
+ mutuallyExclusiveField: NormalizableTransactionParams
103
+ ) {
104
+ if (typeof txParams[mutuallyExclusiveField] !== "undefined") {
105
+ throw rpcErrors.invalidParams(
106
+ `Invalid transaction params: specified ${fieldBeingValidated} but also included ${mutuallyExclusiveField}, these cannot be mixed`
107
+ );
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Ensures that the provided value for field is a string, throws an
113
+ * invalidParams error if field is not a string.
114
+ */
115
+ export function ensureFieldIsString(txParams: TransactionParams, field: NormalizableTransactionParams) {
116
+ if (typeof txParams[field] !== "string") {
117
+ throw rpcErrors.invalidParams(`Invalid transaction params: ${field} is not a string. got: (${txParams[field]})`);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Ensures that the provided txParams has the proper 'type' specified for the
123
+ * given field, if it is provided. If types do not match throws an
124
+ * invalidParams error.
125
+ */
126
+ function ensureProperTransactionEnvelopeTypeProvided(txParams: TransactionParams, field: NormalizableTransactionParams) {
127
+ switch (field) {
128
+ case "maxFeePerGas":
129
+ case "maxPriorityFeePerGas":
130
+ if (txParams.type && txParams.type !== TRANSACTION_ENVELOPE_TYPES.FEE_MARKET) {
131
+ throw rpcErrors.invalidParams(
132
+ `Invalid transaction envelope type: specified type "${txParams.type}" but ` +
133
+ `including maxFeePerGas and maxPriorityFeePerGas requires type: "${TRANSACTION_ENVELOPE_TYPES.FEE_MARKET}"`
134
+ );
135
+ }
136
+ break;
137
+ case "gasPrice":
138
+ default:
139
+ if (txParams.type && txParams.type === TRANSACTION_ENVELOPE_TYPES.FEE_MARKET) {
140
+ throw rpcErrors.invalidParams(
141
+ `Invalid transaction envelope type: specified type "${txParams.type}" but ` +
142
+ "included a gasPrice instead of maxFeePerGas and maxPriorityFeePerGas"
143
+ );
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * validates the from field in txParams
150
+ */
151
+ export function validateFrom(txParams: TransactionParams) {
152
+ if (!(typeof txParams.from === "string")) {
153
+ throw rpcErrors.invalidParams(`Invalid "from" address "${txParams.from}": not a string.`);
154
+ }
155
+ if (!isValidAddress(txParams.from)) {
156
+ throw rpcErrors.invalidParams('Invalid "from" address.');
157
+ }
158
+ }
159
+
160
+ /**
161
+ * validates the to field in txParams
162
+ */
163
+ export function validateRecipient(txParameters: TransactionParams) {
164
+ if (txParameters.to === "0x" || txParameters.to === null) {
165
+ if (txParameters.data) {
166
+ delete txParameters.to;
167
+ } else {
168
+ throw rpcErrors.invalidParams('Invalid "to" address.');
169
+ }
170
+ } else if (txParameters.to !== undefined && !isValidAddress(txParameters.to)) {
171
+ throw rpcErrors.invalidParams('Invalid "to" address.');
172
+ }
173
+ return txParameters;
174
+ }
175
+
176
+ /**
177
+ * Validates the given tx parameters
178
+ * @throws if the tx params contains invalid fields
179
+ */
180
+ export function validateTxParameters(txParams: TransactionParams, eip1559Compatibility = true) {
181
+ if (!txParams || typeof txParams !== "object" || Array.isArray(txParams)) {
182
+ throw rpcErrors.invalidParams("Invalid transaction params: must be an object.");
183
+ }
184
+ if (!txParams.to && !txParams.data) {
185
+ throw rpcErrors.invalidParams(
186
+ 'Invalid transaction params: must specify "data" for contract deployments, or "to" (and optionally "data") for all other types of transactions.'
187
+ );
188
+ }
189
+
190
+ if (isEIP1559Transaction({ transaction: txParams }) && !eip1559Compatibility) {
191
+ throw rpcErrors.invalidParams(
192
+ "Invalid transaction params: params specify an EIP-1559 transaction but the current network does not support EIP-1559"
193
+ );
194
+ }
195
+
196
+ Object.entries(txParams).forEach(([key, value]) => {
197
+ // validate types
198
+ switch (key) {
199
+ case "from":
200
+ validateFrom(txParams);
201
+ break;
202
+ case "to":
203
+ validateRecipient(txParams);
204
+ break;
205
+ case "gasPrice":
206
+ ensureProperTransactionEnvelopeTypeProvided(txParams, "gasPrice");
207
+ ensureMutuallyExclusiveFieldsNotProvided(txParams, "gasPrice", "maxFeePerGas");
208
+ ensureMutuallyExclusiveFieldsNotProvided(txParams, "gasPrice", "maxPriorityFeePerGas");
209
+ ensureFieldIsString(txParams, "gasPrice");
210
+ break;
211
+ case "maxFeePerGas":
212
+ ensureProperTransactionEnvelopeTypeProvided(txParams, "maxFeePerGas");
213
+ ensureMutuallyExclusiveFieldsNotProvided(txParams, "maxFeePerGas", "gasPrice");
214
+ ensureFieldIsString(txParams, "maxFeePerGas");
215
+ break;
216
+ case "maxPriorityFeePerGas":
217
+ ensureProperTransactionEnvelopeTypeProvided(txParams, "maxPriorityFeePerGas");
218
+ ensureMutuallyExclusiveFieldsNotProvided(txParams, "maxPriorityFeePerGas", "gasPrice");
219
+ ensureFieldIsString(txParams, "maxPriorityFeePerGas");
220
+ break;
221
+ case "value":
222
+ ensureFieldIsString(txParams, "value");
223
+ if (value.toString().includes("-")) {
224
+ throw rpcErrors.invalidParams(`Invalid transaction value "${value}": not a positive number.`);
225
+ }
226
+
227
+ if (value.toString().includes(".")) {
228
+ throw rpcErrors.invalidParams(`Invalid transaction value of "${value}": number must be in wei.`);
229
+ }
230
+ break;
231
+ case "chainId":
232
+ if (typeof value !== "number" && typeof value !== "string") {
233
+ throw rpcErrors.invalidParams(`Invalid transaction params: ${key} is not a Number or hex string. got: (${value})`);
234
+ }
235
+ break;
236
+ default:
237
+ ensureFieldIsString(txParams, key as NormalizableTransactionParams);
238
+ }
239
+ });
240
+ }
241
+
242
+ export function normalizeAndValidateTxParams(txParams: TransactionParams, lowerCase = true) {
243
+ const normalizedTxParams = normalizeTxParameters(txParams, lowerCase);
244
+ validateTxParameters(normalizedTxParams);
245
+ return normalizedTxParams;
246
+ }
247
+
248
+ /**
249
+ * @returns an array of states that can be considered final
250
+ */
251
+ export function getFinalStates() {
252
+ return [
253
+ TransactionStatus.rejected, // the user has responded no!
254
+ TransactionStatus.confirmed, // the tx has been included in a block.
255
+ TransactionStatus.failed, // the tx failed for some reason, included on tx data.
256
+ TransactionStatus.dropped, // the tx nonce was already used
257
+ ];
258
+ }
259
+
260
+ export function parseStandardTokenTransactionData(data: string) {
261
+ try {
262
+ const txDesc = erc20Interface.parseTransaction({ data });
263
+ if (txDesc) return { name: txDesc.name, methodParams: txDesc.args.toArray(), type: CONTRACT_TYPE_ERC20 };
264
+ } catch {
265
+ // ignore and next try to parse with erc721 ABI
266
+ }
267
+
268
+ try {
269
+ const txDesc = erc721Interface.parseTransaction({ data });
270
+ if (txDesc) return { name: txDesc.name, methodParams: txDesc.args.toArray(), type: CONTRACT_TYPE_ERC721 };
271
+ } catch {
272
+ // ignore and next try to parse with erc1155 ABI
273
+ }
274
+
275
+ try {
276
+ const txDesc = erc1155Interface.parseTransaction({ data });
277
+ if (txDesc) return { name: txDesc.name, methodParams: txDesc.args.toArray(), type: CONTRACT_TYPE_ERC1155 };
278
+ } catch {
279
+ // ignore and return undefined
280
+ }
281
+
282
+ return undefined;
283
+ }
284
+
285
+ export const readAddressAsContract = async (
286
+ provider: SafeEventEmitterProvider,
287
+ address: string
288
+ ): Promise<{ contractCode: string; isContractAddress: boolean }> => {
289
+ let contractCode;
290
+ try {
291
+ contractCode = await provider.request<[string, string], string>({ method: METHOD_TYPES.ETH_GET_CODE, params: [address, "latest"] });
292
+ } catch (e) {
293
+ contractCode = null;
294
+ }
295
+
296
+ const isContractAddress = contractCode ? contractCode !== "0x" && contractCode !== "0x0" : false;
297
+ return { contractCode, isContractAddress };
298
+ };
299
+
300
+ export async function determineTransactionType(txParams: TransactionParams, provider: SafeEventEmitterProvider) {
301
+ const { data, to } = txParams;
302
+ let name: string = "";
303
+ let methodParams = [];
304
+ let type = "";
305
+ try {
306
+ ({ name, methodParams, type } = (data && parseStandardTokenTransactionData(data)) || {});
307
+ } catch (error) {
308
+ log.debug("Failed to parse transaction data", error);
309
+ }
310
+ let result: TRANSACTION_TYPE;
311
+ let contractCode = "";
312
+ if (data && !to) {
313
+ result = TRANSACTION_TYPES.DEPLOY_CONTRACT;
314
+ } else {
315
+ const { contractCode: resultCode, isContractAddress } = await readAddressAsContract(provider, to);
316
+ contractCode = resultCode;
317
+ if (isContractAddress) {
318
+ const valueExists = txParams.value && Number(txParams.value) !== 0;
319
+ const tokenMethodName: TRANSACTION_TYPE = [
320
+ TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
321
+ TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
322
+ TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
323
+ TRANSACTION_TYPES.COLLECTIBLE_METHOD_SAFE_TRANSFER_FROM,
324
+ TRANSACTION_TYPES.SET_APPROVAL_FOR_ALL,
325
+ ].find((x) => x.toLowerCase() === name?.toLowerCase());
326
+
327
+ result = data && tokenMethodName && !valueExists ? tokenMethodName : TRANSACTION_TYPES.CONTRACT_INTERACTION;
328
+ } else {
329
+ result = TRANSACTION_TYPES.SENT_ETHER;
330
+ }
331
+ }
332
+ return { type: type || CONTRACT_TYPE_ETH, category: result, methodParams, getCodeResponse: contractCode };
333
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ export { default as AccountTrackerController } from "./Account/AccountTrackerController";
2
+ export { default as PollingBlockTracker } from "./Block/PollingBlockTracker";
3
+ export { default as CurrencyController } from "./Currency/CurrencyController";
4
+ export { default as GasFeeController } from "./Gas/GasFeeController";
5
+ export * from "./Gas/IGasFeeController";
6
+ export { default as KeyringController } from "./Keyring/KeyringController";
7
+ export * from "./Message/AbstractMessageController";
8
+ export * from "./Message/DecryptMessageController";
9
+ export * from "./Message/EncryptionPublicKeyController";
10
+ export * from "./Message/MessageController";
11
+ export * from "./Message/PersonalMessageController";
12
+ export * from "./Message/TypedMessageController";
13
+ export * from "./Message/utils";
14
+ export * from "./Network/createEthereumMiddleware";
15
+ export * from "./Network/createJsonRpcClient";
16
+ export { default as NetworkController } from "./Network/NetworkController";
17
+ export * from "./Nfts/INftsController";
18
+ export * from "./Nfts/NftHandler";
19
+ export * from "./Nfts/NftsController";
20
+ export { default as PreferencesController } from "./Preferences/PreferencesController";
21
+ export * from "./Tokens/ITokensController";
22
+ export * from "./Tokens/TokenHandler";
23
+ export * from "./Tokens/TokenRatesController";
24
+ export * from "./Tokens/TokensController";
25
+ export { default as NonceTracker } from "./Transaction/NonceTracker";
26
+ export { default as PendingTransactionTracker } from "./Transaction/PendingTransactionTracker";
27
+ export { default as TransactionController } from "./Transaction/TransactionController";
28
+ export { default as TransactionGasUtil } from "./Transaction/TransactionGasUtil";
29
+ export * from "./Transaction/TransactionStateHistoryHelper";
30
+ export { default as TransactionStateManager } from "./Transaction/TransactionStateManager";
31
+ export * from "./Transaction/TransactionUtils";
32
+ export * from "./utils/constants";
33
+ export * from "./utils/helpers";
34
+ export * from "./utils/interfaces";
35
+
36
+ /**
37
+ * Pending controllers
38
+ * - Transaction Controllers
39
+ // * - AA Controller
40
+ */
41
+
42
+ /**
43
+ * Backend apis
44
+ * - Preferences Controller
45
+ */