@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,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
|
+
*/
|