@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
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
}
|