@exodus/ethereum-api 9.0.0-alpha.3 → 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.
- package/CHANGELOG.md +2121 -0
- package/LICENSE +7 -0
- package/README.md +124 -0
- package/package.json +34 -33
- package/src/address-has-history.js +6 -2
- package/src/allowance/constants.js +4 -5
- package/src/allowance/index.js +159 -40
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +104 -0
- package/src/check-tx/index.js +1 -0
- package/src/create-asset-plugin-factory.js +115 -0
- package/src/create-asset-utils.js +520 -0
- package/src/create-asset.js +272 -97
- package/src/create-token-factory.js +47 -17
- package/src/custom-fees.js +57 -0
- package/src/eip7702-whitelist.js +62 -0
- package/src/ens/index.js +12 -17
- package/src/error-wrapper.js +383 -0
- package/src/eth-like-util.js +172 -33
- package/src/exodus-eth-server/api-coin-nodes.js +27 -196
- package/src/exodus-eth-server/api.js +23 -14
- package/src/exodus-eth-server/clarity-v2.js +255 -0
- package/src/exodus-eth-server/clarity.js +67 -265
- package/src/exodus-eth-server/errors.js +40 -0
- package/src/exodus-eth-server/eth-like-server.js +385 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/exodus-eth-server/index.js +77 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/exodus-eth-server/ws-gateway.js +306 -0
- package/src/exodus-eth-server/ws.js +1 -2
- package/src/fee-data/EthLikeFeeData.js +81 -0
- package/src/fee-data/index.js +54 -0
- package/src/fee-utils.js +229 -0
- package/src/gas-estimation.js +183 -66
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +285 -27
- package/src/get-fee.js +92 -56
- package/src/get-historical-balance.js +58 -0
- package/src/hooks/index.js +1 -1
- package/src/hooks/monitor.js +182 -43
- package/src/hooks/staking-freshness.js +58 -0
- package/src/index.js +118 -12
- package/src/move-funds.js +153 -0
- package/src/multicall3/index.js +212 -0
- package/src/multisig-address.js +146 -0
- package/src/nft-utils.js +60 -0
- package/src/number-utils.js +34 -2
- package/src/optimism-gas/index.js +13 -5
- package/src/send-validations.js +58 -0
- package/src/server-based-fee-monitor.js +12 -19
- package/src/simulate-tx/fetch-tx-preview.js +0 -2
- package/src/simulate-tx/index.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +24 -11
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +108 -0
- package/src/simulation/estimate-fee.js +14 -0
- package/src/simulation/estimate-simple-transfer.js +15 -0
- package/src/simulation/get-message-type.js +18 -0
- package/src/simulation/simulate-message-api.js +68 -0
- package/src/simulation/simulate-transactions-api.js +266 -0
- package/src/simulation/simulate-transactions.js +16 -0
- package/src/simulation/transactions.js +52 -0
- package/src/simulation/try-estimating-changes-locally.js +26 -0
- package/src/staking/api/index.js +43 -0
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +166 -34
- package/src/staking/ethereum/deps.js +37 -0
- package/src/staking/ethereum/everstake.js +113 -0
- package/src/staking/ethereum/index.js +4 -3
- package/src/staking/ethereum/service.js +682 -181
- package/src/staking/ethereum/staking-utils.js +275 -4
- package/src/staking/fantom-staking.js +9 -9
- package/src/staking/index.js +4 -4
- package/src/staking/matic/addresses.js +2 -0
- package/src/staking/matic/api.js +69 -49
- package/src/staking/matic/index.js +3 -3
- package/src/staking/matic/matic-staking-utils.js +243 -5
- package/src/staking/matic/matic-staking.md +53 -0
- package/src/staking/staking-provider-client.js +1 -1
- package/src/staking/utils/index.js +39 -0
- package/src/tx-create.js +666 -0
- package/src/tx-log/clarity-monitor.js +273 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
- package/src/tx-log/clarity-utils/absolute.js +96 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
- package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/clarity-utils/index.js +9 -3
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
- package/src/tx-log/ethereum-monitor.js +93 -47
- package/src/tx-log/ethereum-no-history-monitor.js +52 -41
- package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
- package/src/tx-log/index.js +5 -3
- package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
- package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
- package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
- package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
- package/src/tx-log/monitor-utils/index.js +12 -7
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
- package/src/tx-log-staking-processor/index.js +28 -14
- package/src/tx-log-staking-processor/utils.js +21 -20
- package/src/tx-send/broadcast-error-handler.js +63 -0
- package/src/tx-send/index.js +1 -2
- package/src/tx-send/nonce-utils.js +97 -0
- package/src/tx-send/tx-send.js +94 -288
- package/src/tx-sign/index.js +17 -0
- package/src/tx-type/index.js +280 -0
- package/src/watch-tx.js +36 -0
- package/src/web3/createSimulateMessage.js +3 -2
- package/src/web3/createSimulateTransactions.js +11 -0
- package/src/web3/index.js +3 -1
- package/src/etherscan/account.js +0 -49
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -16
- package/src/etherscan/proxy.js +0 -47
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
- package/src/exodus-eth-server/server-factory.js +0 -28
- package/src/exodus-eth-server/server-map.js +0 -21
- package/src/get-fee-async.js +0 -97
- package/src/staking/matic/service.js +0 -437
- package/src/staking-api.js +0 -5
- package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
- package/src/tx-send/get-fee-info.js +0 -41
- package/src/websocket/index.android.js +0 -2
- package/src/websocket/index.ios.js +0 -2
- 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
|
+
}
|