@exodus/ethereum-api 9.0.0-alpha.3 → 9.0.1

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 +2139 -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 +273 -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 +276 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +130 -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
package/src/index.js CHANGED
@@ -1,15 +1,121 @@
1
1
  // Ideally, we should reduce the exports to just api.
2
2
  // If the client is importing one module specifically,
3
3
  // it's breaking the encapsulation
4
- export * from './eth-like-util'
5
- export * from './gas-estimation'
6
- export { getServer, getServerByName } from './exodus-eth-server/server-map'
7
- export * from './tx-log'
8
- export * from './get-balances'
9
- export * from './staking'
10
- export * from './simulate-tx'
11
- export * from './allowance'
12
- export * from './optimism-gas'
13
- export * from './number-utils'
14
- export { txSendFactory, getFeeInfo } from './tx-send'
15
- export { createAssetFactory } from './create-asset'
4
+ export {
5
+ FORWARDER_CONTRACT_CODE,
6
+ isContractAddress,
7
+ isContractAddressCached,
8
+ isForwarderContract,
9
+ isForwarderContractCached,
10
+ getNonce,
11
+ estimateGas,
12
+ getBalance,
13
+ getBalanceProxied,
14
+ getTokenBalance,
15
+ getTokenBalanceFromNode,
16
+ transactionExists,
17
+ getTransaction,
18
+ getIsForwarderContract,
19
+ getERC20Params,
20
+ getEIP7702Delegation,
21
+ } from './eth-like-util.js'
22
+
23
+ export {
24
+ DEFAULT_CONTRACT_GAS_LIMIT,
25
+ DEFAULT_GAS_LIMIT_MULTIPLIER,
26
+ estimateGasLimit,
27
+ fetchGasLimit,
28
+ scaleGasLimitEstimate,
29
+ } from './gas-estimation.js'
30
+
31
+ export { createEvmServer, getServer } from './exodus-eth-server/index.js'
32
+
33
+ export {
34
+ EthereumMonitor,
35
+ EthereumNoHistoryMonitor,
36
+ ClarityMonitor,
37
+ getOptimisticTxLogEffects,
38
+ } from './tx-log/index.js'
39
+
40
+ export { getStakingHistoryBalance, getBalancesFactory } from './get-balances.js'
41
+ export { getHistoricalBalanceFactory } from './get-historical-balance.js'
42
+ export { createEncodeMultisigContract } from './multisig-address.js'
43
+
44
+ export { FantomStaking, stakingProviderClientFactory } from './staking/index.js'
45
+ export { EthLikeRpcRequestAccumulator } from './multicall3/index.js'
46
+ export { createCachedStakingRead } from './staking/cached-read.js'
47
+ export { ethereumStakingDeps } from './staking/ethereum/deps.js'
48
+ export { getEthereumStakingInfo, createEthereumStakingService } from './staking/ethereum/service.js'
49
+ export { EthereumStaking } from './staking/ethereum/api.js'
50
+ export {
51
+ isEthereumStakingTx,
52
+ isEthereumDelegate,
53
+ isEthereumUndelegatePending,
54
+ isEthereumUndelegate,
55
+ isEthereumClaimUndelegate,
56
+ } from './staking/ethereum/staking-utils.js'
57
+ export { MaticStakingApi } from './staking/matic/index.js'
58
+ export {
59
+ EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
60
+ STAKING_MANAGER_ADDR,
61
+ } from './staking/matic/addresses.js'
62
+ export {
63
+ isPolygonTx,
64
+ isPolygonDelegate,
65
+ isPolygonUndelegate,
66
+ isPolygonReward,
67
+ isPolygonClaimUndelegate,
68
+ isPendingPolygonUndelegateTxInEthereumTxLog,
69
+ } from './staking/matic/matic-staking-utils.js'
70
+
71
+ export { fetchTxPreview } from './simulate-tx/fetch-tx-preview.js'
72
+ export { maybeRemoveDuplicates, retrieveSideEffects } from './simulate-tx/simulate-eth-tx.js'
73
+ export { simulateTransactions } from './simulation/simulate-transactions.js'
74
+
75
+ export {
76
+ isZeroAllowanceAsset,
77
+ getSpendingAllowance,
78
+ isSpendingApprovalRequired,
79
+ buildApproveTx,
80
+ buildLegacyApproveTx,
81
+ createApprove,
82
+ createApproveSpendingTokens,
83
+ APPROVAL_GAS_LIMIT,
84
+ ZERO_ALLOWANCE_ASSETS,
85
+ } from './allowance/index.js'
86
+
87
+ export { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
88
+
89
+ export { getAggregateTransactionPricing } from './get-fee.js'
90
+
91
+ export {
92
+ fromHexToString,
93
+ fromHexToBigInt,
94
+ addHexPrefix,
95
+ removeHexPrefix,
96
+ splitIn32BytesArray,
97
+ removeLeadingZeroes,
98
+ fromHexToBN,
99
+ } from './number-utils.js'
100
+
101
+ export {
102
+ EVM_ERROR_REASONS,
103
+ EVM_ERROR_TYPES,
104
+ getEvmErrorReason,
105
+ withErrorReason,
106
+ EthLikeError,
107
+ } from './error-wrapper.js'
108
+
109
+ export { txSendFactory } from './tx-send/index.js'
110
+
111
+ export { createAssetFactory } from './create-asset.js'
112
+ export { createCheckTx } from './check-tx/index.js'
113
+
114
+ export { moveFundsFactory } from './move-funds.js'
115
+
116
+ export { createContractBlackListCheck } from './create-asset-utils.js'
117
+
118
+ export {
119
+ createAssetPluginFactory,
120
+ fromAddEthereumChainParameterToFactoryParams,
121
+ } from './create-asset-plugin-factory.js'
@@ -0,0 +1,153 @@
1
+ import { signUnsignedTx } from '@exodus/ethereum-lib'
2
+ import { addHexPrefix, isValidPrivate, privateToAddress, toBuffer } from '@exodus/ethereumjs/util'
3
+ import assert from 'minimalistic-assert'
4
+
5
+ import { getNonce, getTokenBalanceFromNode } from './eth-like-util.js'
6
+ import { fetchGasLimit } from './gas-estimation.js'
7
+ import { AmountIncludesFeeUnderflowError } from './tx-create.js'
8
+
9
+ export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx, server }) => {
10
+ assert(baseAssetName, 'baseAssetName is required')
11
+ assert(assetClientInterface, 'assetClientInterface is required')
12
+ assert(createTx, 'createTx is required')
13
+ assert(server, 'server is required')
14
+
15
+ async function prepareSendFundsTx({
16
+ assetName,
17
+ input,
18
+ toAddress,
19
+ walletAccount,
20
+ MoveFundsError,
21
+ }) {
22
+ assert(assetName, 'assetName is required')
23
+ assert(input, 'input is required')
24
+
25
+ assert(walletAccount, 'walletAccount is required')
26
+ assert(MoveFundsError, 'MoveFundsError is required')
27
+
28
+ const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName })
29
+ const asset = assets[assetName]
30
+ assert(asset, 'asset is required')
31
+
32
+ assert(toAddress && asset.baseAsset.address.validate(toAddress), 'valid toAddress is required')
33
+
34
+ let privateKey
35
+ try {
36
+ const hexKey = addHexPrefix(input.trim())
37
+ if (typeof hexKey !== 'string' || hexKey.length !== 66) throw new Error('invalid length')
38
+ privateKey = toBuffer(hexKey)
39
+ if (!isValidPrivate(privateKey)) throw new Error('invalid key')
40
+ } catch {
41
+ throw new MoveFundsError('private-key-invalid')
42
+ }
43
+
44
+ const fromAddress = `0x${privateToAddress(privateKey).toString('hex')}`
45
+
46
+ if (fromAddress === toAddress.toLowerCase()) {
47
+ throw new MoveFundsError('private-key-own-key', { fromAddress })
48
+ }
49
+
50
+ const isToken = asset.name !== baseAssetName
51
+
52
+ const [nonce, rawEthBalance] = await Promise.all([
53
+ getNonce({ asset: asset.baseAsset, address: fromAddress }),
54
+ server.getBalanceProxied(fromAddress),
55
+ ])
56
+
57
+ const ethBalance = asset.baseAsset.currency.baseUnit(rawEthBalance)
58
+
59
+ let amount = ethBalance
60
+ if (isToken) {
61
+ const rawTokenBalance = await getTokenBalanceFromNode({ asset, address: fromAddress })
62
+ amount = asset.currency.baseUnit(rawTokenBalance)
63
+ }
64
+
65
+ if (amount.isZero) {
66
+ throw new MoveFundsError('balance-zero', { fromAddress })
67
+ }
68
+
69
+ const feeData = await assetClientInterface.getFeeData({ assetName })
70
+
71
+ const gasLimit = await fetchGasLimit({
72
+ asset,
73
+ feeData,
74
+ fromAddress,
75
+ toAddress,
76
+ amount,
77
+ // `isSendAll` and `amountIncludesFee` are no-ops on the token branch
78
+ // (both gated on `!isToken` / `!isEthereumLikeToken(asset)` downstream),
79
+ // so don't pass them. Keeps the API surface honest about which flags
80
+ // actually take effect for each branch.
81
+ ...(isToken ? null : { isSendAll: true }),
82
+ })
83
+
84
+ let createTxResult
85
+ try {
86
+ createTxResult = await createTx({
87
+ asset,
88
+ walletAccount,
89
+ fromAddress,
90
+ // Native sweep: pass the full balance and let createTx subtract the
91
+ // accurate fee. Token sweep: pass the full token balance unchanged
92
+ // (the native gas fee is paid separately from native balance).
93
+ address: toAddress,
94
+ amount,
95
+ nonce,
96
+ gasLimit,
97
+ gasPrice: feeData.gasPrice,
98
+ // TODO: drop this. Move funds has no fee picker, so it's always
99
+ // `feeData.gasPrice` (same as `gasPrice` above) and a no-op anyway.
100
+ // If we ever need a custom fee, thread it through prepareSendFundsTx.
101
+ customFee: feeData.gasPrice,
102
+ // Both flags are no-ops for tokens (see fetchGasLimit call above).
103
+ ...(isToken ? null : { isSendAll: true, amountIncludesFee: true }),
104
+ })
105
+ } catch (err) {
106
+ // createTx throws `AmountIncludesFeeUnderflowError` for the native
107
+ // sweep when the accurate fee (including OP-stack L1 data fee) exceeds
108
+ // the balance we passed in. Translate to the typed MoveFundsError so
109
+ // consumers (mobile/desktop MoveFunds screens) render the right copy.
110
+ if (!isToken && err instanceof AmountIncludesFeeUnderflowError) {
111
+ throw new MoveFundsError('balance-negative', { fromAddress })
112
+ }
113
+
114
+ throw err
115
+ }
116
+
117
+ // Token sweep equivalent of the native underflow gate: createTx doesn't
118
+ // validate ETH balance vs gas for token transfers, so check it here using
119
+ // the authoritative fee returned from `calculateFee`. This catches the
120
+ // OP-stack edge case where balance covers `gasPrice * gasLimit` but not
121
+ // the additional L1 data fee.
122
+ if (isToken && ethBalance.sub(createTxResult.fee).isNegative) {
123
+ throw new MoveFundsError('token-fee-insufficient', { fromAddress })
124
+ }
125
+
126
+ // Trust createTx for both amount and fee. For native sweep `amount` is
127
+ // the post-subtraction value (input balance minus accurate fee). For
128
+ // token sweep `amount` is the input token balance unchanged, since we
129
+ // don't pass `amountIncludesFee`. In both cases `fee` includes the
130
+ // OP-stack L1 data fee from `calculateFee`, which matters more for
131
+ // tokens than for native because ERC-20 calldata is larger.
132
+ return {
133
+ fromAddress,
134
+ toAddress,
135
+ amount: createTxResult.amount,
136
+ fee: createTxResult.fee,
137
+ privateKey,
138
+ unsignedTx: createTxResult.unsignedTx,
139
+ }
140
+ }
141
+
142
+ const sendFunds = async ({ privateKey, unsignedTx }) => {
143
+ assert(privateKey, 'privateKey is required')
144
+ assert(unsignedTx, 'unsignedTx is required')
145
+
146
+ const { rawTx } = await signUnsignedTx(unsignedTx, privateKey)
147
+ const txId = await server.sendRawTransaction(rawTx.toString('hex'))
148
+
149
+ return { txId }
150
+ }
151
+
152
+ return { prepareSendFundsTx, sendFunds }
153
+ }
@@ -0,0 +1,212 @@
1
+ import { memoize } from '@exodus/basic-utils'
2
+ import { ABI } from '@exodus/ethereum-lib'
3
+ import { bufferToHex } from '@exodus/ethereumjs/util'
4
+ import SolidityContract from '@exodus/solidity-contract'
5
+ import assert from 'minimalistic-assert'
6
+
7
+ import { EthLikeServerBase } from '../exodus-eth-server/eth-like-server.js'
8
+ import { BLOCK_TAG_LATEST } from '../tx-send/nonce-utils.js'
9
+
10
+ const assertValidMulticall3BlockTag = (blockTag) =>
11
+ assert(blockTag === BLOCK_TAG_LATEST, 'blockTag must be "latest" for multicall3')
12
+
13
+ const SUPPORTED_METHODS = new Set(['eth_getBalance', 'eth_blockNumber', 'eth_call'])
14
+
15
+ const assertSupportedMethod = (method) => {
16
+ assert(
17
+ SUPPORTED_METHODS.has(method),
18
+ `method "${method}" is not supported for static call batching`
19
+ )
20
+ }
21
+
22
+ const getMulticall3Contract = memoize(() => new SolidityContract(ABI.multicall3))
23
+
24
+ export class EthLikeRpcRequestAccumulator extends EthLikeServerBase {
25
+ static REQUEST_PARSER_NOOP = ({ result }) => result
26
+
27
+ static REQUEST_PARSER_UINT256 = ({ result }) => `0x${BigInt(result).toString(16)}`
28
+
29
+ static REQUEST_PARSER_BALANCE_OF = ({ originalRequest, result }) => {
30
+ const {
31
+ params: [{ to: tokenAddress }],
32
+ } = originalRequest
33
+
34
+ return {
35
+ confirmed: {
36
+ [tokenAddress]: BigInt(result).toString(),
37
+ },
38
+ }
39
+ }
40
+
41
+ #requestParser = null
42
+ #requests = []
43
+
44
+ #withRequestParser(functionName, requestParser) {
45
+ assert(typeof functionName === 'string', 'expected functionName')
46
+ assert(typeof requestParser === 'function', 'expected requestParser')
47
+
48
+ return (...args) => {
49
+ assert(!this.#requestParser) // invariant
50
+
51
+ this.#requestParser = requestParser
52
+ const result = super[functionName](...args)
53
+ this.#requestParser = null
54
+
55
+ return result
56
+ }
57
+ }
58
+
59
+ _serializeRequest({ originalRequest }) {
60
+ return super.buildRequest(originalRequest)
61
+ }
62
+
63
+ async _execute({ requests, server }) {
64
+ return server.sendBatchRequest(requests.map(({ request }) => request))
65
+ }
66
+
67
+ buildRequest(originalRequest) {
68
+ const { method, params } = originalRequest
69
+ assertSupportedMethod(method)
70
+
71
+ if (method === 'eth_getBalance' || method === 'eth_call') {
72
+ const tag = params[1]
73
+ assertValidMulticall3BlockTag(tag)
74
+ }
75
+
76
+ if (method === 'eth_call') {
77
+ const { from } = params[0]
78
+ assert(!from, 'cannot specify "from"')
79
+ }
80
+
81
+ void this.#requests.push({
82
+ originalRequest,
83
+ request: this._serializeRequest({ originalRequest }),
84
+ requestParser: this.#requestParser ?? null,
85
+ })
86
+
87
+ return this
88
+ }
89
+
90
+ balanceOfRequest(...args) {
91
+ return this.#withRequestParser(
92
+ 'balanceOfRequest',
93
+ EthLikeRpcRequestAccumulator.REQUEST_PARSER_BALANCE_OF
94
+ )(...args)
95
+ }
96
+
97
+ blockNumberRequest(...args) {
98
+ return this.#withRequestParser(
99
+ 'blockNumberRequest',
100
+ EthLikeRpcRequestAccumulator.REQUEST_PARSER_UINT256
101
+ )(...args)
102
+ }
103
+
104
+ getBalanceRequest(...args) {
105
+ return this.#withRequestParser(
106
+ 'getBalanceRequest',
107
+ EthLikeRpcRequestAccumulator.REQUEST_PARSER_UINT256
108
+ )(...args)
109
+ }
110
+
111
+ contractStaticCallFromRequest(contract, functionName, ...args) {
112
+ const requestParser = ({ result }) => contract[functionName].parse(result)
113
+ return this.#withRequestParser('contractStaticCallFromRequest', requestParser)(
114
+ contract,
115
+ functionName,
116
+ ...args
117
+ )
118
+ }
119
+
120
+ contractStaticCallRequest(contract, functionName, ...args) {
121
+ return this.contractStaticCallFromRequest(contract, functionName, undefined, ...args)
122
+ }
123
+
124
+ length() {
125
+ return this.#requests.length
126
+ }
127
+
128
+ reset() {
129
+ this.#requests.length = 0
130
+ }
131
+
132
+ async flush(server) {
133
+ assert(server, 'expected server')
134
+
135
+ const requests = [...this.#requests]
136
+ this.reset()
137
+
138
+ if (requests.length === 0) return []
139
+
140
+ const results = await this._execute({ requests, server })
141
+
142
+ return results.map((result, i) => {
143
+ const request = requests[i]
144
+ assert(request)
145
+
146
+ const { requestParser: maybeRequestParser, originalRequest } = request
147
+
148
+ const requestParser = maybeRequestParser ?? EthLikeRpcRequestAccumulator.REQUEST_PARSER_NOOP
149
+
150
+ return requestParser({ originalRequest, result })
151
+ })
152
+ }
153
+ }
154
+
155
+ export class EthLikeMulticall3RpcRequestAccumulator extends EthLikeRpcRequestAccumulator {
156
+ #multicall3Address
157
+ #multicall3Contract
158
+
159
+ constructor({ multicall3Address }) {
160
+ super()
161
+
162
+ assert(typeof multicall3Address === 'string')
163
+ this.#multicall3Address = multicall3Address
164
+ this.#multicall3Contract = getMulticall3Contract()
165
+ }
166
+
167
+ _serializeRequest({ originalRequest: { method, params } }) {
168
+ switch (method) {
169
+ case 'eth_getBalance':
170
+ return {
171
+ address: this.#multicall3Address,
172
+ calldata: bufferToHex(this.#multicall3Contract.getEthBalance.build(params[0])),
173
+ }
174
+ case 'eth_blockNumber':
175
+ return {
176
+ address: this.#multicall3Address,
177
+ calldata: bufferToHex(this.#multicall3Contract.getBlockNumber.build()),
178
+ }
179
+ case 'eth_call': {
180
+ const [data] = params
181
+ const { to, data: calldata } = data
182
+ return { address: to, calldata }
183
+ }
184
+
185
+ default:
186
+ assert(false)
187
+ }
188
+ }
189
+
190
+ async _execute({ requests, server }) {
191
+ const data = await server.ethCall({
192
+ data: bufferToHex(
193
+ this.#multicall3Contract.aggregate.build(
194
+ requests.map(({ request: { address, calldata } }) => [address, calldata])
195
+ )
196
+ ),
197
+ to: this.#multicall3Address,
198
+ })
199
+
200
+ const multicall3Result = this.#multicall3Contract.decodeOutput({ data, method: 'aggregate' })
201
+
202
+ assert(
203
+ Array.isArray(multicall3Result) && multicall3Result.length === 2,
204
+ 'invalid multicall3Result'
205
+ )
206
+
207
+ const results = multicall3Result[1].map((buf) => bufferToHex(buf))
208
+ assert(results.length === requests.length)
209
+
210
+ return results
211
+ }
212
+ }
@@ -0,0 +1,146 @@
1
+ import { memoize } from '@exodus/basic-utils'
2
+ import { hash } from '@exodus/crypto/hash'
3
+ import { publicKeyIsValid } from '@exodus/crypto/secp256k1'
4
+ import { toChecksumAddress } from '@exodus/ethereumjs/util'
5
+ import SolidityContract from '@exodus/solidity-contract'
6
+
7
+ const keccak256 = async (data) => hash('keccak256', data)
8
+
9
+ // Limit multisig keys to 16 for now
10
+ const MAX_PUBKEYS = 16
11
+
12
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
13
+
14
+ // This derives the counterfactual CREATE2 address of OUR canonical Safe 4337
15
+ // deployment (fixed singleton, modules and salt nonce, so owners + threshold
16
+ // fully determine the address). It intentionally does NOT resolve addresses of
17
+ // arbitrary pre-existing Safes, whose setup we cannot reconstruct from pubkeys.
18
+ // https://docs.safe.global/advanced/erc-4337/guides/permissionless-detailed
19
+
20
+ // Safe{Core} Protocol deployments for the Safe 4337 setup, chainId 1.
21
+ // https://github.com/safe-global/safe-modules-deployments
22
+ const ADD_MODULES_LIB_ADDRESS = '0x8EcD4ec46D4D2a6B64fE960B3D64e8B94B2234eb'
23
+ const SAFE_4337_MODULE_ADDRESS = '0xa581c4A4DB7175302464fF3C06380BC3270b4037'
24
+ const PROXY_FACTORY_ADDRESS = '0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67'
25
+
26
+ // enableModules([SAFE_4337_MODULE_ADDRESS]) calldata passed to setup()
27
+ const SETUP_DATA = `0x8d0dc49f00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000${SAFE_4337_MODULE_ADDRESS.slice(2)}`
28
+
29
+ // SALT_NONCE is protocol-kit's getChainSpecificDefaultSaltNonce(1n). Careful
30
+ // reproducing it: the derivation is stringly, NOT abi.encodePacked of numbers.
31
+ // PREDETERMINED_SALT_NONCE is a hex *string*, `+ chainId` is JS *string
32
+ // concatenation*, and toHex() *utf8-encodes* the resulting text before hashing:
33
+ //
34
+ // PREDETERMINED_SALT_NONCE = '0x' + keccak256(utf8('Safe Account Abstraction'))
35
+ // = '0xb1073742015cbcf5a3a4d9d1ae33ecf619439710b89475f92e2abd2117e90f90'
36
+ // SALT_NONCE = keccak256(utf8(PREDETERMINED_SALT_NONCE + '1')) // chainId 1
37
+ //
38
+ // i.e. keccak256 of the 67 ASCII characters '0xb107...f901'
39
+ const SALT_NONCE = '0x69b348339eea4ed93f9d11931c3b894c8f9d8c7663a053024b11cb7eb4e5a1f6'
40
+
41
+ // SafeProxy creation code with the SafeL2 singleton (0x41675C09...) constructor argument appended
42
+ const INIT_CODE =
43
+ '0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f766964656400000000000000000000000041675c099f32341bf84bfc5382af534df5c7461a'
44
+
45
+ const SAFE_SETUP_ABI = [
46
+ {
47
+ inputs: [
48
+ { internalType: 'address[]', name: '_owners', type: 'address[]' },
49
+ { internalType: 'uint256', name: '_threshold', type: 'uint256' },
50
+ { internalType: 'address', name: 'to', type: 'address' },
51
+ { internalType: 'bytes', name: 'data', type: 'bytes' },
52
+ { internalType: 'address', name: 'fallbackHandler', type: 'address' },
53
+ { internalType: 'address', name: 'paymentToken', type: 'address' },
54
+ { internalType: 'uint256', name: 'payment', type: 'uint256' },
55
+ { internalType: 'address payable', name: 'paymentReceiver', type: 'address' },
56
+ ],
57
+ name: 'setup',
58
+ outputs: [],
59
+ stateMutability: 'nonpayable',
60
+ type: 'function',
61
+ },
62
+ ]
63
+
64
+ const getSafeContract = memoize(() => new SolidityContract(SAFE_SETUP_ABI, '', true))
65
+
66
+ const SALT_NONCE_BUFFER = Buffer.from(SALT_NONCE.slice(2), 'hex')
67
+
68
+ export const createEncodeMultisigContract = ({ chainId, encodePublic }) => {
69
+ // The Safe deployment constants above (addresses and SALT_NONCE) only derive
70
+ // valid addresses for chainId 1, so refuse other chains instead of silently
71
+ // deriving an incorrect address
72
+ if (chainId !== 1) {
73
+ throw new Error(`asset.encodeMultisigContract is not supported for chainId ${chainId}`)
74
+ }
75
+
76
+ return async (publicKeys, meta = Object.create(null)) => {
77
+ if (
78
+ !Array.isArray(publicKeys) ||
79
+ publicKeys.some((k) => !Buffer.isBuffer(k) || !publicKeyIsValid({ publicKey: k }))
80
+ ) {
81
+ throw new Error('publicKeys must be an Array of Buffers representing valid public keys')
82
+ }
83
+
84
+ const { threshold = publicKeys.length, version = 0 } = meta
85
+
86
+ if (publicKeys.length <= 0 || publicKeys.length > MAX_PUBKEYS) {
87
+ throw new Error(`asset.encodeMultisigContract supports from 1 to ${MAX_PUBKEYS} pubKeys`)
88
+ }
89
+
90
+ if (!Number.isSafeInteger(version)) {
91
+ throw new TypeError('asset.encodeMultisigContract requires meta.version to be an integer')
92
+ }
93
+
94
+ // Only support version 0 for now
95
+ if (version !== 0) {
96
+ throw new Error(`asset.encodeMultisigContract does not support version ${version}`)
97
+ }
98
+
99
+ if (!Number.isSafeInteger(threshold) || threshold <= 0 || threshold > MAX_PUBKEYS) {
100
+ throw new Error(
101
+ `asset.encodeMultisigContract requires meta.threshold to be an integer between 1 and ${MAX_PUBKEYS}`
102
+ )
103
+ }
104
+
105
+ if (threshold > publicKeys.length) {
106
+ throw new Error('threshold must be <= publicKeys.length')
107
+ }
108
+
109
+ // Owners are intentionally NOT sorted: the Safe address depends on the owner
110
+ // order in setup(), so publicKeys must be passed in a stable, agreed-upon order
111
+ const owners = publicKeys.map((publicKey) => encodePublic(publicKey))
112
+
113
+ if (new Set(owners.map((o) => o.toLowerCase())).size !== owners.length) {
114
+ throw new Error('publicKeys must not contain any duplicates')
115
+ }
116
+
117
+ const setupData = getSafeContract().setup.build(
118
+ owners,
119
+ threshold,
120
+ ADD_MODULES_LIB_ADDRESS,
121
+ SETUP_DATA,
122
+ SAFE_4337_MODULE_ADDRESS,
123
+ ZERO_ADDRESS,
124
+ 0,
125
+ ZERO_ADDRESS
126
+ )
127
+
128
+ // CREATE2: keccak256(0xff ++ proxyFactory ++ keccak256(keccak256(setupData) ++ saltNonce) ++ keccak256(initCode))[12:]
129
+ const [setupDataHash, initCodeHash] = await Promise.all([
130
+ keccak256(setupData),
131
+ keccak256(Buffer.from(INIT_CODE.slice(2), 'hex')),
132
+ ])
133
+
134
+ const salt = await keccak256([setupDataHash, SALT_NONCE_BUFFER])
135
+
136
+ const create2Hash = await keccak256([
137
+ Buffer.from('ff', 'hex'),
138
+ Buffer.from(PROXY_FACTORY_ADDRESS.slice(2), 'hex'),
139
+ salt,
140
+ initCodeHash,
141
+ ])
142
+ const address = create2Hash.slice(12)
143
+
144
+ return { address: toChecksumAddress('0x' + address.toString('hex')) }
145
+ }
146
+ }