@exodus/ethereum-api 9.0.0-alpha.2 → 9.0.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 (143) hide show
  1. package/CHANGELOG.md +2121 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +272 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. package/src/websocket/index.js +0 -21
@@ -0,0 +1,115 @@
1
+ import { createMetaDef } from '@exodus/asset'
2
+ import assert from 'minimalistic-assert'
3
+
4
+ import { createAssetFactory } from './create-asset.js'
5
+
6
+ // Move to ethereum-api for config base evm
7
+ export const createAssetPluginFactory = (config) => {
8
+ assert(config, 'config is required')
9
+ assert(config.chainId, 'chainId is required')
10
+ assert(config.meta, 'config.meta is required')
11
+ assert(config.meta.explorerUrl, 'meta.explorerUrl is required')
12
+
13
+ const chainId = config.chainId
14
+ const { explorerUrl, decimals, ...meta } = config.meta
15
+ const chainIdHex = '0x' + chainId.toString(16)
16
+ const name = `evm${chainIdHex}`.toLowerCase()
17
+ const ticker = name.toUpperCase()
18
+ const tokenType = `EVM_${chainIdHex}_TOKEN`.toUpperCase()
19
+
20
+ const units = {
21
+ wei: 0,
22
+ Kwei: 3,
23
+ Mwei: 6,
24
+ Gwei: 9,
25
+ [ticker]: decimals || 18,
26
+ }
27
+
28
+ // adds lots of validation in config
29
+ const tokenOverrides = (token) => ({
30
+ ...token,
31
+ assetId: token.addresses.current.toLowerCase(),
32
+ assetType: tokenType,
33
+ contract: token.addresses,
34
+ })
35
+
36
+ const blockExplorer = {
37
+ addressUrl: (address) => `${explorerUrl}/address/${encodeURIComponent(address)}`,
38
+ txUrl: (txId) => `${explorerUrl}/tx/${encodeURIComponent(txId)}`,
39
+ }
40
+
41
+ const assetType = 'ETHEREUM_LIKE'
42
+ const { assetsList } = createMetaDef({
43
+ assetParams: {
44
+ ...meta,
45
+ name,
46
+ ticker,
47
+ units,
48
+ chainId,
49
+ assetType,
50
+ blockExplorer,
51
+ },
52
+ tokensParams: meta.tokens || [],
53
+ tokenOverrides,
54
+ })
55
+ const feeDataConfig = Object.fromEntries(
56
+ Object.entries({
57
+ gasPrice: config.gasPrice,
58
+ baseFeePerGas: config.baseFeePerGas,
59
+ eip1559Enabled: config.eip1559Enabled,
60
+ ...config.feeData,
61
+ }).filter(([_, value]) => value !== undefined)
62
+ )
63
+
64
+ const createAsset = createAssetFactory({
65
+ assetsList,
66
+ feeDataConfig,
67
+ ...config.plugin,
68
+ })
69
+
70
+ return { name, chainId, createAsset }
71
+ }
72
+
73
+ // https://docs.metamask.io/wallet/reference/wallet_addethereumchain/
74
+ export const fromAddEthereumChainParameterToFactoryParams = (params) => {
75
+ assert(params.chainId, 'chainId is required')
76
+ assert(params.chainName, 'chainName is required')
77
+ assert(params.rpcUrls?.[0], 'rpcUrls is required')
78
+ assert(params.blockExplorerUrls?.[0], 'blockExplorerUrls is required')
79
+ assert(params.nativeCurrency, 'nativeCurrency is required')
80
+ assert(params.nativeCurrency.symbol, 'nativeCurrency.symbol is required')
81
+ assert(params.nativeCurrency.decimals, 'nativeCurrency.decimals is required')
82
+
83
+ const chainId = parseInt(params.chainId.replace('0x', ''), 16)
84
+
85
+ // obvious coloring:)
86
+ const color =
87
+ params.color ||
88
+ '#' +
89
+ (chainId % 256).toString(16) +
90
+ ((chainId + 50) % 256).toString(16) +
91
+ ((chainId + 100) % 256).toString(16)
92
+ return {
93
+ chainId,
94
+ meta: {
95
+ explorerUrl: params.blockExplorerUrls[0],
96
+ displayNetworkName: params.chainName,
97
+ displayName: params.nativeCurrency.name || params.nativeCurrency.symbol,
98
+ displayTicker: params.nativeCurrency.symbol,
99
+ decimals: params.nativeCurrency.decimals,
100
+ primaryColor: color,
101
+ gradientColors: [color, color],
102
+ },
103
+ plugin: {
104
+ serverUrl: params.rpcUrls?.[0],
105
+ confirmationsNumber: 5,
106
+ monitorType: 'no-history',
107
+ isTestnet: params.isTestnet,
108
+ ...params.plugin, // extra plugin configuration
109
+ },
110
+ feeData: params.feeData,
111
+ baseFeePerGas: params.baseFeePerGas,
112
+ gasPrice: params.gasPrice,
113
+ eip1559Enabled: params.eip1559Enabled,
114
+ }
115
+ }
@@ -0,0 +1,520 @@
1
+ import { BlacklistCheckTypes } from '@exodus/asset-lib'
2
+ import { parseUnsignedTx } from '@exodus/ethereum-lib'
3
+ import { defaultAbiCoder } from '@exodus/ethereumjs/ethers5-abi'
4
+ import { safeString } from '@exodus/safe-string'
5
+ import assert from 'minimalistic-assert'
6
+ import ms from 'ms'
7
+
8
+ import { EVM_ERROR_REASONS, withErrorReason } from './error-wrapper.js'
9
+ import ClarityServerV2 from './exodus-eth-server/clarity-v2.js'
10
+ import { ValidMonitorTypes } from './exodus-eth-server/index.js'
11
+ import { createEthereumHooks } from './hooks/index.js'
12
+ import {
13
+ EthLikeMulticall3RpcRequestAccumulator,
14
+ EthLikeRpcRequestAccumulator,
15
+ } from './multicall3/index.js'
16
+ import { ClarityMonitor } from './tx-log/clarity-monitor.js'
17
+ import { ClarityTruncatedHistoryMonitor } from './tx-log/clarity-truncated-history-monitor.js'
18
+ import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
19
+ import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
20
+ import { getOptimisticTxLogEffects } from './tx-log/get-optimistic-txlog-effects.js'
21
+ import { BLOCK_TAG_LATEST, BLOCK_TAG_PENDING, resolveNonce } from './tx-send/nonce-utils.js'
22
+ import { signTx } from './tx-sign/index.js'
23
+
24
+ // Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
25
+ // to use for a given config.
26
+ export const resolveMonitorSettings = (
27
+ {
28
+ asset,
29
+ configWithOverrides,
30
+ defaultMonitorInterval,
31
+ defaultMonitorType,
32
+ defaultServerUrl,
33
+ defaultWsGatewayUri,
34
+ } = Object.create(null)
35
+ ) => {
36
+ assert(asset, 'expected asset')
37
+ assert(
38
+ ValidMonitorTypes.includes(defaultMonitorType),
39
+ `defaultMonitorType ${defaultMonitorType} for ${asset.name} is invalid.`
40
+ )
41
+ assert(defaultServerUrl, `expected default serverUrl for ${asset.name}`)
42
+
43
+ // NOTE: When trying to enable `clarity-v3` as an asset
44
+ // default config, there must exist a default
45
+ // gateway definition.
46
+ if (defaultMonitorType === 'clarity-v3') {
47
+ assert(defaultWsGatewayUri, 'expected defaultWsGatewayUri')
48
+ }
49
+
50
+ const overrideMonitorType = configWithOverrides?.monitorType
51
+ const overrideServerUrl = configWithOverrides?.serverUrl
52
+ const overrideMonitorInterval = configWithOverrides?.monitorInterval
53
+ const overrideWsGatewayUri = configWithOverrides?.wsGatewayUri
54
+
55
+ const defaultResolution = {
56
+ // NOTE: Regardless of the `monitorType`, the `monitorInterval`
57
+ // will be respected.
58
+ monitorInterval: overrideMonitorInterval ?? defaultMonitorInterval,
59
+ monitorType: defaultMonitorType,
60
+ serverUrl: defaultServerUrl,
61
+ wsGatewayUri: defaultWsGatewayUri,
62
+ }
63
+
64
+ const fallbackWithWarning = (msg) => {
65
+ console.debug([msg, `Falling back to ${defaultMonitorType}<${defaultServerUrl}>.`].join(' '))
66
+ return defaultResolution
67
+ }
68
+
69
+ if (!overrideMonitorType && overrideServerUrl) {
70
+ return fallbackWithWarning(
71
+ `Received an \`overrideServerUrl\`, but not the \`monitorType\` for ${asset.name}.`
72
+ )
73
+ }
74
+
75
+ // If we don't attempt to override the `monitorType`, we resort
76
+ // to the default configuration.
77
+ if (!overrideMonitorType) return defaultResolution
78
+
79
+ if (!ValidMonitorTypes.includes(overrideMonitorType)) {
80
+ return fallbackWithWarning(`"${overrideMonitorType}" is not a valid \`MonitorType\`.`)
81
+ }
82
+
83
+ const wsGatewayUri = overrideWsGatewayUri || defaultWsGatewayUri
84
+
85
+ if (overrideMonitorType === 'clarity-v3' && !wsGatewayUri) {
86
+ return fallbackWithWarning(
87
+ "Attempted to use 'clarity-v3' without a defining a supporting wsGatewayUri."
88
+ )
89
+ }
90
+
91
+ // Permit the `monitorType` and `serverUrl` to be overrided.
92
+ return {
93
+ ...defaultResolution,
94
+ monitorType: overrideMonitorType,
95
+ serverUrl: overrideServerUrl,
96
+ wsGatewayUri,
97
+ }
98
+ }
99
+
100
+ export const stringifyPrivateTx = (tx) => {
101
+ assert(tx, 'expected tx')
102
+ if (tx instanceof Uint8Array) return `0x${Buffer.from(tx).toString('hex')}`
103
+
104
+ assert(typeof tx === 'string', 'expected string')
105
+ if (tx.startsWith('0x')) return tx
106
+
107
+ return `0x${tx}`
108
+ }
109
+
110
+ const assertSendPrivateTxProps = ({ asset, unsignedTx, walletAccount }) => {
111
+ assert(asset, 'expected asset')
112
+ assert(unsignedTx, 'expected unsignedTx')
113
+ assert(walletAccount, 'expected walletAccount')
114
+ }
115
+
116
+ // Block builders drop private transactions that carry no bid — accepted by
117
+ // the relay but never included on chain (ExodusMovement/assets#8084). The
118
+ // effective bid is min(tipGasPrice, gasPrice - baseFeePerGas): `gasPrice` is
119
+ // the maxFeePerGas ceiling for EIP-1559 transactions and the all-in price
120
+ // for legacy ones, so either term at zero kills the bid.
121
+ const assertNonZeroTipGasPrice = async ({ asset, assetClientInterface, unsignedTx }) => {
122
+ const { eip1559Enabled, gasPrice, tipGasPrice } = parseUnsignedTx({ asset, unsignedTx })
123
+
124
+ if (eip1559Enabled && (!tipGasPrice || tipGasPrice.isZero)) {
125
+ throw new Error(
126
+ 'ERR_PRIVATE_TX_ZERO_TIP: transaction pays no tipGasPrice; block builders drop zero-bid private transactions'
127
+ )
128
+ }
129
+
130
+ const feeData = await assetClientInterface.getFeeConfig({ assetName: asset.baseAsset.name })
131
+ const baseFeePerGas = feeData?.baseFeePerGas
132
+ if (!baseFeePerGas || gasPrice.gt(baseFeePerGas)) return
133
+
134
+ throw new Error(
135
+ `ERR_PRIVATE_TX_ZERO_TIP: transaction gasPrice (${gasPrice.toBaseString()}) does not exceed baseFeePerGas (${baseFeePerGas.toBaseString()}); block builders drop zero-bid private transactions`
136
+ )
137
+ }
138
+
139
+ export const createTransactionPrivacyResult = ({
140
+ assetClientInterface,
141
+ broadcastPrivateBundle,
142
+ broadcastPrivateTx,
143
+ }) => {
144
+ assert(assetClientInterface, 'expected assetClientInterface')
145
+ assert(typeof broadcastPrivateBundle === 'function')
146
+ assert(typeof broadcastPrivateTx === 'function')
147
+
148
+ const sendPrivateTx = async ({ asset, unsignedTx, walletAccount }) => {
149
+ assertSendPrivateTxProps({ asset, unsignedTx, walletAccount })
150
+ await assertNonZeroTipGasPrice({ asset, assetClientInterface, unsignedTx })
151
+
152
+ const { rawTx, txId } = await signTx({
153
+ asset,
154
+ assetClientInterface,
155
+ unsignedTx,
156
+ walletAccount,
157
+ })
158
+
159
+ await broadcastPrivateTx(rawTx.toString('hex'))
160
+
161
+ const fromAddress = unsignedTx.txMeta?.fromAddress
162
+ assert(typeof fromAddress === 'string', 'expected fromAddress')
163
+
164
+ const { nonce, optimisticTxLogEffects } = await getOptimisticTxLogEffects({
165
+ asset,
166
+ assetClientInterface,
167
+ fromAddress,
168
+ txId,
169
+ unsignedTx,
170
+ walletAccount,
171
+ })
172
+
173
+ for (const optimisticTxLogEffect of optimisticTxLogEffects) {
174
+ await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
175
+ }
176
+
177
+ return { nonce, txId }
178
+ }
179
+
180
+ const sendPrivateBundle = async ({ unsignedTxBundle }) => {
181
+ assert(
182
+ Array.isArray(unsignedTxBundle) && unsignedTxBundle.length > 0,
183
+ 'expected non-empty unsignedTxBundle'
184
+ )
185
+
186
+ unsignedTxBundle.forEach(assertSendPrivateTxProps)
187
+
188
+ for (const { asset, unsignedTx } of unsignedTxBundle) {
189
+ await assertNonZeroTipGasPrice({ asset, assetClientInterface, unsignedTx })
190
+ }
191
+
192
+ const signTxResults = await Promise.all(
193
+ unsignedTxBundle.map(({ asset, unsignedTx, walletAccount }) =>
194
+ signTx({ assetClientInterface, asset, unsignedTx, walletAccount })
195
+ )
196
+ )
197
+
198
+ const maybeBundleResult = await broadcastPrivateBundle({
199
+ txs: signTxResults.map(({ rawTx }) => rawTx.toString('hex')),
200
+ })
201
+
202
+ const bundleHash = maybeBundleResult?.bundleHash || undefined
203
+
204
+ const getOptimisticTxLogEffectsResults = []
205
+
206
+ for (const [i, { txId }] of signTxResults.entries()) {
207
+ const { asset, unsignedTx, walletAccount } = unsignedTxBundle[i]
208
+
209
+ const fromAddress = unsignedTx.txMeta?.fromAddress
210
+ assert(typeof fromAddress === 'string', 'expected fromAddress')
211
+
212
+ void getOptimisticTxLogEffectsResults.push(
213
+ await getOptimisticTxLogEffects({
214
+ asset,
215
+ assetClientInterface,
216
+ fromAddress,
217
+ txId,
218
+ unsignedTx,
219
+ walletAccount,
220
+ // TODO: use consistent naming
221
+ bundleId: bundleHash,
222
+ })
223
+ )
224
+ }
225
+
226
+ const optimisticTxLogEffects = getOptimisticTxLogEffectsResults.flatMap(
227
+ ({ optimisticTxLogEffects }) => optimisticTxLogEffects
228
+ )
229
+
230
+ for (const optimisticTxLogEffect of optimisticTxLogEffects) {
231
+ await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
232
+ }
233
+
234
+ return {
235
+ bundleHash,
236
+ txIds: signTxResults.map(({ txId }) => txId),
237
+ }
238
+ }
239
+
240
+ return {
241
+ broadcastPrivateBundle,
242
+ broadcastPrivateTx,
243
+ sendPrivateTx,
244
+ sendPrivateBundle,
245
+ }
246
+ }
247
+
248
+ export const createTransactionPrivacyFactory = ({
249
+ assetClientInterface,
250
+ assetName,
251
+ server,
252
+ supportsTransactionPrivacy,
253
+ }) => {
254
+ assert(assetClientInterface, 'expected assetClientInterface')
255
+ assert(server, 'expected server')
256
+
257
+ if (!supportsTransactionPrivacy) return Object.create(null)
258
+
259
+ if (!(server instanceof ClarityServerV2)) {
260
+ console.warn(
261
+ `attempted to enable transactionPrivacy for ${assetName} but provided server is incompatible`
262
+ )
263
+ return Object.create(null)
264
+ }
265
+
266
+ const broadcastPrivateBundle = async ({ txs }) => {
267
+ assert(Array.isArray(txs), 'txs must be an array')
268
+ assert(txs.length > 0, 'txs must be a non-empty array')
269
+
270
+ const sendBundleResult = await server.sendBundle({
271
+ txs: txs.map((tx) => stringifyPrivateTx(tx)),
272
+ })
273
+ if (typeof sendBundleResult === 'string') return { bundleHash: sendBundleResult }
274
+
275
+ const bundleHash = sendBundleResult?.bundleHash
276
+ if (typeof bundleHash === 'string') return { bundleHash }
277
+
278
+ console.warn('unexpected sendBundleResult shape, cannot determine bundle hash')
279
+ return null
280
+ }
281
+
282
+ const broadcastPrivateTx = (tx) => broadcastPrivateBundle({ txs: [tx] })
283
+
284
+ return createTransactionPrivacyResult({
285
+ assetClientInterface,
286
+ broadcastPrivateBundle,
287
+ broadcastPrivateTx,
288
+ })
289
+ }
290
+
291
+ export const createRpcRequestAccumulatorFactory =
292
+ ({ multicall3Address }) =>
293
+ () =>
294
+ multicall3Address
295
+ ? new EthLikeMulticall3RpcRequestAccumulator({ multicall3Address })
296
+ : new EthLikeRpcRequestAccumulator()
297
+
298
+ export const createHistoryMonitorFactory = ({
299
+ assetName,
300
+ assetClientInterface,
301
+ monitorInterval,
302
+ monitorType,
303
+ server,
304
+ stakingConfiguration,
305
+ rpcBalanceAssetNames,
306
+ eip7702Whitelist,
307
+ getBlackListStatus,
308
+ }) => {
309
+ assert(assetName, 'expected assetName')
310
+ assert(assetClientInterface, 'expected assetClientInterface')
311
+ assert(monitorType, 'expected monitorType')
312
+ assert(server, 'expected server')
313
+ assert(
314
+ stakingConfiguration &&
315
+ typeof stakingConfiguration === 'object' &&
316
+ !Array.isArray(stakingConfiguration),
317
+ 'expected object stakingConfiguration'
318
+ )
319
+
320
+ return (args) => {
321
+ let monitor
322
+ switch (monitorType) {
323
+ case 'clarity-v3':
324
+ console.debug('clarity-v3 is no longer supported, falling back to clarity-v2')
325
+ // eslint-disable-next-line no-fallthrough
326
+ case 'clarity':
327
+ case 'clarity-v2':
328
+ monitor = new ClarityMonitor({
329
+ assetClientInterface,
330
+ interval: ms(monitorInterval || '5m'),
331
+ server,
332
+ rpcBalanceAssetNames,
333
+ eip7702Whitelist,
334
+ getBlackListStatus,
335
+ ...args,
336
+ })
337
+ break
338
+ case 'clarity-truncated-history':
339
+ monitor = new ClarityTruncatedHistoryMonitor({
340
+ assetClientInterface,
341
+ interval: ms(monitorInterval || '5m'),
342
+ server,
343
+ eip7702Whitelist,
344
+ getBlackListStatus,
345
+ ...args,
346
+ })
347
+ break
348
+ case 'no-history':
349
+ monitor = new EthereumNoHistoryMonitor({
350
+ assetClientInterface,
351
+ interval: ms(monitorInterval || '15s'),
352
+ server,
353
+ eip7702Whitelist,
354
+ getBlackListStatus,
355
+ ...args,
356
+ })
357
+ break
358
+ case 'magnifier':
359
+ monitor = new EthereumMonitor({
360
+ assetClientInterface,
361
+ interval: ms(monitorInterval || '15s'),
362
+ server,
363
+ rpcBalanceAssetNames,
364
+ ...args,
365
+ })
366
+ break
367
+ default:
368
+ throw new Error(`Monitor type ${monitorType} of evm asset ${assetName} is unknown`)
369
+ }
370
+
371
+ if (Object.keys(stakingConfiguration).length > 0) {
372
+ const hooks = createEthereumHooks({
373
+ assetClientInterface,
374
+ assetName,
375
+ stakingConfiguration,
376
+ server,
377
+ })
378
+ for (const [name, hook] of Object.entries(hooks)) monitor.addHook(name, hook)
379
+ }
380
+
381
+ return monitor
382
+ }
383
+ }
384
+
385
+ export const createSecurityChecks = ({ eip7702Whitelist }) => {
386
+ return ({ accountState }) => {
387
+ // Always return global scam findings before lower-severity checks because
388
+ // global checks can block the app at startup.
389
+ if (accountState?.isBlacklisted) {
390
+ return {
391
+ isSecure: false,
392
+ type: 'GLOBAL_SCAM',
393
+ reason: 'Account is globally blacklisted.',
394
+ }
395
+ }
396
+
397
+ const delegation = accountState?.eip7702Delegation
398
+ if (
399
+ eip7702Whitelist.isSupported() &&
400
+ Boolean(delegation?.isDelegated) &&
401
+ !delegation?.isWhitelisted
402
+ ) {
403
+ return {
404
+ isSecure: false,
405
+ type: 'LOST_PERMISSIONS',
406
+ reason: 'Account is delegated to a non-whitelisted EIP-7702 address.',
407
+ }
408
+ }
409
+
410
+ return {
411
+ isSecure: true,
412
+ type: null,
413
+ reason: null,
414
+ }
415
+ }
416
+ }
417
+
418
+ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNonce }) => {
419
+ assert(assetClientInterface, 'expected assetClientInterface')
420
+ assert(typeof useAbsoluteBalanceAndNonce === 'boolean', 'expected useAbsoluteBalanceAndNonce')
421
+
422
+ const getNonce = async ({
423
+ asset,
424
+ fromAddress,
425
+ walletAccount,
426
+ forceFromNode,
427
+ // NOTE: By default, Exodus assumes the client will by default want
428
+ // to send transactions on top of those which are currently
429
+ // pending at the public mempool.
430
+ tag = BLOCK_TAG_PENDING,
431
+ }) => {
432
+ assert(asset, 'expected asset')
433
+ assert(typeof fromAddress === 'string', 'expected fromAddress')
434
+ assert(walletAccount, 'expected walletAccount')
435
+
436
+ const [txLog, accountState] = await Promise.all([
437
+ assetClientInterface.getTxLog({
438
+ assetName: asset.baseAsset.name,
439
+ walletAccount,
440
+ }),
441
+ assetClientInterface.getAccountState({
442
+ assetName: asset.baseAsset.name,
443
+ walletAccount,
444
+ }),
445
+ ])
446
+
447
+ return resolveNonce({
448
+ asset,
449
+ fromAddress,
450
+ txLog,
451
+ accountState,
452
+ forceFromNode,
453
+ tag,
454
+ useAbsoluteNonce: useAbsoluteBalanceAndNonce,
455
+ })
456
+ }
457
+
458
+ return { getNonce }
459
+ }
460
+
461
+ // Creates a contract-based blacklist check function.
462
+ // async ({ address }) => boolean
463
+ export const createContractBlackListCheck = ({ contractAddress, selector, server }) => {
464
+ assert(contractAddress, 'expected contractAddress')
465
+ assert(selector, 'expected selector')
466
+ assert(server, 'expected server')
467
+
468
+ return async ({ address }) => {
469
+ const tag = BLOCK_TAG_LATEST
470
+ const encodedAddress = address.slice(2).toLowerCase().padStart(64, '0')
471
+ const data = selector + encodedAddress
472
+ const result = await withErrorReason({
473
+ promise: server.ethCall({ to: contractAddress, data }, tag),
474
+ errorReasonInfo: EVM_ERROR_REASONS.ethCallErc20Failed,
475
+ hint: safeString`getBlackListStatus:ethCall`,
476
+ baseAssetName: server.baseAssetName,
477
+ })
478
+
479
+ if (typeof result !== 'string' || result.length === 0) {
480
+ throw new Error('getBlackListStatus got invalid ethCall response')
481
+ }
482
+
483
+ let isBlacklisted
484
+ try {
485
+ ;[isBlacklisted] = defaultAbiCoder.decode(['bool'], result)
486
+ } catch (err) {
487
+ throw new Error(`getBlackListStatus got invalid ABI bool result: ${err.message}`)
488
+ }
489
+
490
+ return isBlacklisted
491
+ }
492
+ }
493
+
494
+ // Composes blacklist check factories into a single getBlackListStatus.
495
+ // Returns undefined if no checks are provided.
496
+ // Returns { isBlacklisted: true } if ANY check returns true.
497
+ // NOTE: blacklistChecks defaults to [] as a convenience, but it is the asset's responsibility
498
+ // to be properly configured with the appropriate checks. An empty array means no blacklist
499
+ // checks are run, which implies EIP-7702 is effectively enabled on that network with no whitelist.
500
+ export const createGetBlackListStatus = ({ address: addressApi, blacklistChecks = [], server }) => {
501
+ if (blacklistChecks.length === 0) return
502
+
503
+ assert(addressApi, 'expected address')
504
+
505
+ // Extend here to support additional blacklist sources (e.g. an internal database query).
506
+ const checks = blacklistChecks.map((factory) => {
507
+ if (factory.type === BlacklistCheckTypes.CONTRACT_CALL) {
508
+ assert(server, 'expected server for CONTRACT_CALL blacklist check')
509
+ return factory({ server })
510
+ }
511
+
512
+ throw new Error(`Unknown blacklist check type: ${factory.type}`)
513
+ })
514
+
515
+ return async ({ address }) => {
516
+ assert(addressApi.validate(address), 'getBlackListStatus requires a valid Ethereum address')
517
+ const results = await Promise.all(checks.map((check) => check({ address })))
518
+ return { isBlacklisted: results.some(Boolean) }
519
+ }
520
+ }