@exodus/ethereum-api 8.77.0 → 8.77.2
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 +24 -0
- package/package.json +4 -4
- package/src/create-asset.js +9 -0
- package/src/exodus-eth-server/api-coin-nodes.js +1 -11
- package/src/exodus-eth-server/clarity.js +1 -12
- package/src/exodus-eth-server/eth-like-server-base.js +35 -0
- package/src/index.js +1 -0
- package/src/multicall3/index.js +68 -28
- package/src/multisig-address.js +146 -0
- package/src/simulation/create-simulate-transactions.js +2 -0
- package/src/simulation/simulate-transactions-api.js +19 -18
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [8.77.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.77.2) (2026-07-10)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [8.77.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.0...@exodus/ethereum-api@8.77.1) (2026-07-09)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add contract `staticcall` helper for `rpcRequestAccumulator` ([#8336](https://github.com/ExodusMovement/assets/issues/8336)) ([674f1a3](https://github.com/ExodusMovement/assets/commit/674f1a39c0a8fd5353b6ae7a19b79166f5aabfc9))
|
|
20
|
+
* include recipient address state changes in simulation result (EVM + Solana) ([#8321](https://github.com/ExodusMovement/assets/issues/8321)) ([556bfde](https://github.com/ExodusMovement/assets/commit/556bfded420ca37692e53865cafd5688557ed283))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **ethereum-api:** guard batch RPC against non-array responses ([#8350](https://github.com/ExodusMovement/assets/issues/8350)) ([683dbab](https://github.com/ExodusMovement/assets/commit/683dbabf32ffd7c03a649c6b037cfed4665f108d))
|
|
26
|
+
* lower ETH min staking amount ([#8319](https://github.com/ExodusMovement/assets/issues/8319)) ([461e4c4](https://github.com/ExodusMovement/assets/commit/461e4c4f46e58e3750363edb9d0b6cddbfae5478))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
## [8.77.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.7...@exodus/ethereum-api@8.77.0) (2026-06-29)
|
|
7
31
|
|
|
8
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.77.
|
|
3
|
+
"version": "8.77.2",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@exodus/asset": "^2.0.4",
|
|
26
|
-
"@exodus/asset-lib": "^5.9.
|
|
26
|
+
"@exodus/asset-lib": "^5.9.2",
|
|
27
27
|
"@exodus/assets": "^11.4.0",
|
|
28
28
|
"@exodus/basic-utils": "^3.0.1",
|
|
29
29
|
"@exodus/bip44-constants": "^195.0.0",
|
|
30
30
|
"@exodus/crypto": "^1.0.0-rc.26",
|
|
31
31
|
"@exodus/currency": "^6.0.1",
|
|
32
|
-
"@exodus/ethereum-lib": "^5.
|
|
32
|
+
"@exodus/ethereum-lib": "^5.25.0",
|
|
33
33
|
"@exodus/ethereum-meta": "^2.9.1",
|
|
34
34
|
"@exodus/ethereumholesky-meta": "^2.0.5",
|
|
35
35
|
"@exodus/ethereumjs": "^1.11.0",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"type": "git",
|
|
70
70
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "c9b1bbede2e2e7b0f2359ec736e3e92f402b2eba"
|
|
73
73
|
}
|
package/src/create-asset.js
CHANGED
|
@@ -43,6 +43,7 @@ import { getBalancesFactory } from './get-balances.js'
|
|
|
43
43
|
import { getFeeFactory } from './get-fee.js'
|
|
44
44
|
import { getHistoricalBalanceFactory } from './get-historical-balance.js'
|
|
45
45
|
import { moveFundsFactory } from './move-funds.js'
|
|
46
|
+
import { createEncodeMultisigContract } from './multisig-address.js'
|
|
46
47
|
import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
|
|
47
48
|
import { createSendValidations } from './send-validations.js'
|
|
48
49
|
import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
|
|
@@ -172,6 +173,12 @@ export const createAssetFactory = ({
|
|
|
172
173
|
encodePublic: encodePublicFactory({ chainId, useEip1191ChainIdChecksum }),
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
// The Safe 4337 contract constants baked into the derivation are chainId 1 specific
|
|
177
|
+
const encodeMultisigContract =
|
|
178
|
+
chainId === 1
|
|
179
|
+
? createEncodeMultisigContract({ chainId, encodePublic: keys.encodePublic })
|
|
180
|
+
: undefined
|
|
181
|
+
|
|
175
182
|
const getBalances = getBalancesFactory({
|
|
176
183
|
monitorType,
|
|
177
184
|
useAbsoluteBalance: useAbsoluteBalanceAndNonce,
|
|
@@ -238,6 +245,7 @@ export const createAssetFactory = ({
|
|
|
238
245
|
isMaxFeeAsset,
|
|
239
246
|
isTestnet,
|
|
240
247
|
moveFunds: true,
|
|
248
|
+
multisig: Boolean(encodeMultisigContract),
|
|
241
249
|
nfts,
|
|
242
250
|
noHistory: monitorType === 'no-history',
|
|
243
251
|
signWithSigner: true,
|
|
@@ -399,6 +407,7 @@ export const createAssetFactory = ({
|
|
|
399
407
|
address,
|
|
400
408
|
api,
|
|
401
409
|
chainId,
|
|
410
|
+
...(encodeMultisigContract && { encodeMultisigContract }),
|
|
402
411
|
monitorType,
|
|
403
412
|
estimateL1DataFee,
|
|
404
413
|
forceGasLimitEstimation,
|
|
@@ -34,17 +34,7 @@ export default class ApiCoinNodesServer extends EthLikeServerBase {
|
|
|
34
34
|
async sendBatchRequest(batch) {
|
|
35
35
|
if (isEmpty(batch)) return batch
|
|
36
36
|
const responses = await this.request(batch)
|
|
37
|
-
|
|
38
|
-
return !isNaN(response?.id) && response?.result !== undefined
|
|
39
|
-
})
|
|
40
|
-
if (responses.length !== batch.length || !isValid) {
|
|
41
|
-
throw new Error('Bad rpc batch response')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const keyed = responses.reduce((acc, response) => {
|
|
45
|
-
return { ...acc, [`${response.id}`]: response.result }
|
|
46
|
-
}, Object.create(null))
|
|
47
|
-
return batch.map((request) => keyed[`${request.id}`])
|
|
37
|
+
return this.parseBatchResponse({ batch, responses, errorMessage: 'Bad rpc batch response' })
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
async sendRequest(request) {
|
|
@@ -216,18 +216,7 @@ export default class ClarityServer extends EthLikeServerBase {
|
|
|
216
216
|
// Transport: WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
217
217
|
async sendBatchRequest(batch) {
|
|
218
218
|
const responses = await this.sendRpcRequest(batch)
|
|
219
|
-
|
|
220
|
-
const isValid = responses.every((response) => {
|
|
221
|
-
return !isNaN(response?.id) && response?.result !== undefined
|
|
222
|
-
})
|
|
223
|
-
if (responses.length !== batch.length || !isValid) {
|
|
224
|
-
throw new Error('Bad Response')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const keyed = responses.reduce((acc, response) => {
|
|
228
|
-
return { ...acc, [`${response.id}`]: response.result }
|
|
229
|
-
}, Object.create(null))
|
|
230
|
-
return batch.map((request) => keyed[`${request.id}`])
|
|
219
|
+
return this.parseBatchResponse({ batch, responses, errorMessage: 'Bad Response' })
|
|
231
220
|
}
|
|
232
221
|
|
|
233
222
|
// Transport: Uses sendRpcRequest - WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
@@ -120,4 +120,39 @@ export default class EthLikeServerBase extends EventEmitter {
|
|
|
120
120
|
assert(Array.isArray(txs), 'expected array txs')
|
|
121
121
|
return this.buildRequest({ method: 'eth_sendBundle', params: [{ txs }] })
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validates a JSON-RPC batch response, returning each `result` ordered to
|
|
126
|
+
* match `batch` (correlated by `id`, since responses can arrive out of order).
|
|
127
|
+
*
|
|
128
|
+
* @param {Array<{ id: number|string }>} args.batch - Requests sent, in the order results are expected.
|
|
129
|
+
* @param {unknown} args.responses - Raw transport payload; expected `[{ id, result }]` but treated as untrusted (gateways may return `{ message }` on error).
|
|
130
|
+
* @param {string} args.errorMessage - Thrown-error prefix per transport (e.g. `'Bad rpc batch response'`, `'Bad Response'`).
|
|
131
|
+
* @returns {unknown[]} Each request's `result`, ordered to match `batch`.
|
|
132
|
+
* @throws {Error} When `responses` isn't an array, length mismatches `batch`, or any entry lacks a numeric `id` / defined `result`.
|
|
133
|
+
*/
|
|
134
|
+
parseBatchResponse({ batch, responses, errorMessage }) {
|
|
135
|
+
// Gateways can respond with a JSON object (e.g. `{ message }`) instead of a batch array.
|
|
136
|
+
if (!Array.isArray(responses)) {
|
|
137
|
+
// Untrusted server text: keep only a short string preview, developer-facing (no `hint`).
|
|
138
|
+
const message = [responses?.message, responses?.error?.message].find(
|
|
139
|
+
(value) => typeof value === 'string'
|
|
140
|
+
)
|
|
141
|
+
const preview = message ? `: ${message.slice(0, 100)}` : ''
|
|
142
|
+
throw new Error(`${errorMessage}${preview}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isValid = responses.every(
|
|
146
|
+
(response) => !isNaN(response?.id) && response?.result !== undefined
|
|
147
|
+
)
|
|
148
|
+
if (responses.length !== batch.length || !isValid) {
|
|
149
|
+
throw new Error(errorMessage)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Safe against prototype keys: every `id` is validated numeric by the `isValid` check above.
|
|
153
|
+
const keyed = Object.fromEntries(
|
|
154
|
+
responses.map((response) => [`${response.id}`, response.result])
|
|
155
|
+
)
|
|
156
|
+
return batch.map((request) => keyed[`${request.id}`])
|
|
157
|
+
}
|
|
123
158
|
}
|
package/src/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
|
|
40
40
|
export { getStakingHistoryBalance, getBalancesFactory } from './get-balances.js'
|
|
41
41
|
export { getHistoricalBalanceFactory } from './get-historical-balance.js'
|
|
42
|
+
export { createEncodeMultisigContract } from './multisig-address.js'
|
|
42
43
|
|
|
43
44
|
export {
|
|
44
45
|
FantomStaking,
|
package/src/multicall3/index.js
CHANGED
|
@@ -22,20 +22,41 @@ const assertSupportedMethod = (method) => {
|
|
|
22
22
|
const getMulticall3Contract = memoize(() => new SolidityContract(ABI.multicall3))
|
|
23
23
|
|
|
24
24
|
export class EthLikeRpcRequestAccumulator extends EthLikeServerBase {
|
|
25
|
-
|
|
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
|
|
26
42
|
#requests = []
|
|
27
43
|
|
|
28
|
-
#
|
|
29
|
-
assert(
|
|
30
|
-
|
|
44
|
+
#withRequestParser(functionName, requestParser) {
|
|
45
|
+
assert(typeof functionName === 'string', 'expected functionName')
|
|
46
|
+
assert(typeof requestParser === 'function', 'expected requestParser')
|
|
31
47
|
|
|
32
|
-
|
|
48
|
+
return (...args) => {
|
|
49
|
+
assert(!this.#requestParser) // invariant
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
this.#requestParser = requestParser
|
|
52
|
+
const result = super[functionName](...args)
|
|
53
|
+
this.#requestParser = null
|
|
54
|
+
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
36
57
|
}
|
|
37
58
|
|
|
38
|
-
_serializeRequest({
|
|
59
|
+
_serializeRequest({ originalRequest }) {
|
|
39
60
|
return super.buildRequest(originalRequest)
|
|
40
61
|
}
|
|
41
62
|
|
|
@@ -52,19 +73,49 @@ export class EthLikeRpcRequestAccumulator extends EthLikeServerBase {
|
|
|
52
73
|
assertValidMulticall3BlockTag(tag)
|
|
53
74
|
}
|
|
54
75
|
|
|
55
|
-
const context = this.#requestContext ?? method
|
|
56
|
-
|
|
57
76
|
void this.#requests.push({
|
|
58
|
-
context,
|
|
59
77
|
originalRequest,
|
|
60
|
-
request: this._serializeRequest({
|
|
78
|
+
request: this._serializeRequest({ originalRequest }),
|
|
79
|
+
requestParser: this.#requestParser ?? null,
|
|
61
80
|
})
|
|
62
81
|
|
|
63
82
|
return this
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
balanceOfRequest(...args) {
|
|
67
|
-
return this.#
|
|
86
|
+
return this.#withRequestParser(
|
|
87
|
+
'balanceOfRequest',
|
|
88
|
+
EthLikeRpcRequestAccumulator.REQUEST_PARSER_BALANCE_OF
|
|
89
|
+
)(...args)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
blockNumberRequest(...args) {
|
|
93
|
+
return this.#withRequestParser(
|
|
94
|
+
'blockNumberRequest',
|
|
95
|
+
EthLikeRpcRequestAccumulator.REQUEST_PARSER_UINT256
|
|
96
|
+
)(...args)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getBalanceRequest(...args) {
|
|
100
|
+
return this.#withRequestParser(
|
|
101
|
+
'getBalanceRequest',
|
|
102
|
+
EthLikeRpcRequestAccumulator.REQUEST_PARSER_UINT256
|
|
103
|
+
)(...args)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
contractStaticCallRequest(contract, functionName, ...args) {
|
|
107
|
+
assert(contract instanceof SolidityContract, 'expected SolidityContract contract')
|
|
108
|
+
assert(typeof functionName === 'string', 'expected string functionName')
|
|
109
|
+
|
|
110
|
+
const requestParser = ({ result }) => contract[functionName].parse(result)
|
|
111
|
+
|
|
112
|
+
return this.#withRequestParser(
|
|
113
|
+
'ethCallRequest',
|
|
114
|
+
requestParser
|
|
115
|
+
)({
|
|
116
|
+
to: contract.address,
|
|
117
|
+
data: bufferToHex(contract[functionName].build(...args)),
|
|
118
|
+
})
|
|
68
119
|
}
|
|
69
120
|
|
|
70
121
|
length() {
|
|
@@ -89,22 +140,11 @@ export class EthLikeRpcRequestAccumulator extends EthLikeServerBase {
|
|
|
89
140
|
const request = requests[i]
|
|
90
141
|
assert(request)
|
|
91
142
|
|
|
92
|
-
const {
|
|
143
|
+
const { requestParser: maybeRequestParser, originalRequest } = request
|
|
93
144
|
|
|
94
|
-
|
|
95
|
-
if (context === 'eth_blockNumber') return `0x${BigInt(result).toString(16)}`
|
|
96
|
-
if (context === 'balanceOfRequest') {
|
|
97
|
-
const {
|
|
98
|
-
params: [{ to: tokenAddress }],
|
|
99
|
-
} = originalRequest
|
|
100
|
-
return {
|
|
101
|
-
confirmed: {
|
|
102
|
-
[tokenAddress]: BigInt(result).toString(),
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
}
|
|
145
|
+
const requestParser = maybeRequestParser ?? EthLikeRpcRequestAccumulator.REQUEST_PARSER_NOOP
|
|
106
146
|
|
|
107
|
-
return result
|
|
147
|
+
return requestParser({ originalRequest, result })
|
|
108
148
|
})
|
|
109
149
|
}
|
|
110
150
|
}
|
|
@@ -121,7 +161,7 @@ export class EthLikeMulticall3RpcRequestAccumulator extends EthLikeRpcRequestAcc
|
|
|
121
161
|
this.#multicall3Contract = getMulticall3Contract()
|
|
122
162
|
}
|
|
123
163
|
|
|
124
|
-
_serializeRequest({
|
|
164
|
+
_serializeRequest({ originalRequest: { method, params } }) {
|
|
125
165
|
switch (method) {
|
|
126
166
|
case 'eth_getBalance':
|
|
127
167
|
return {
|
|
@@ -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
|
+
}
|
|
@@ -12,6 +12,7 @@ export const createSimulateTransactions =
|
|
|
12
12
|
asset,
|
|
13
13
|
origin,
|
|
14
14
|
blockNumber,
|
|
15
|
+
recipientAddress,
|
|
15
16
|
headers: headersFromParams,
|
|
16
17
|
overrideApiEndpoint,
|
|
17
18
|
}) => {
|
|
@@ -56,6 +57,7 @@ export const createSimulateTransactions =
|
|
|
56
57
|
apiEndpoint: overrideApiEndpoint || apiEndpoint,
|
|
57
58
|
origin,
|
|
58
59
|
blockNumber,
|
|
60
|
+
recipientAddress,
|
|
59
61
|
simulationResult,
|
|
60
62
|
headers,
|
|
61
63
|
})
|
|
@@ -3,7 +3,7 @@ import { createCurrency, makeSimulationAPICall } from '@exodus/web3-utils'
|
|
|
3
3
|
|
|
4
4
|
import { BLOWFISH_EVM_CHAINS, MAX_INT256_SOLIDITY } from './common.js'
|
|
5
5
|
|
|
6
|
-
const convertTransactionToApiPayload = (transactions, origin, blockNumber) => {
|
|
6
|
+
const convertTransactionToApiPayload = (transactions, origin, blockNumber, recipientAddress) => {
|
|
7
7
|
const payload = {
|
|
8
8
|
txObjects: transactions.map((transaction) => ({
|
|
9
9
|
from: transaction.from,
|
|
@@ -13,9 +13,7 @@ const convertTransactionToApiPayload = (transactions, origin, blockNumber) => {
|
|
|
13
13
|
gas: transaction.gas,
|
|
14
14
|
gas_price: transaction.gasPrice,
|
|
15
15
|
})),
|
|
16
|
-
metadata: {
|
|
17
|
-
origin,
|
|
18
|
-
},
|
|
16
|
+
metadata: recipientAddress ? { origin, recipientAddress } : { origin },
|
|
19
17
|
userAccount: transactions[0].from,
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -156,6 +154,7 @@ export const simulateTransactionsApi = async ({
|
|
|
156
154
|
apiEndpoint,
|
|
157
155
|
origin,
|
|
158
156
|
blockNumber,
|
|
157
|
+
recipientAddress,
|
|
159
158
|
simulationResult,
|
|
160
159
|
headers,
|
|
161
160
|
asset,
|
|
@@ -174,7 +173,12 @@ export const simulateTransactionsApi = async ({
|
|
|
174
173
|
|
|
175
174
|
const userAddress = transactions[0].from
|
|
176
175
|
|
|
177
|
-
const payload = convertTransactionToApiPayload(
|
|
176
|
+
const payload = convertTransactionToApiPayload(
|
|
177
|
+
transactions,
|
|
178
|
+
origin,
|
|
179
|
+
blockNumber,
|
|
180
|
+
recipientAddress
|
|
181
|
+
)
|
|
178
182
|
|
|
179
183
|
if (!Object.hasOwnProperty.call(BLOWFISH_EVM_CHAINS, asset.name)) {
|
|
180
184
|
return successDefaultResult
|
|
@@ -240,24 +244,21 @@ export const simulateTransactionsApi = async ({
|
|
|
240
244
|
return successDefaultResult
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const result =
|
|
249
|
-
(Object.prototype.hasOwnProperty.call(expectedStateChanges, address) &&
|
|
250
|
-
expectedStateChanges[address]) ||
|
|
251
|
-
[]
|
|
247
|
+
const addressesToProcess = [userAddress.toLowerCase()]
|
|
248
|
+
if (recipientAddress && recipientAddress.toLowerCase() !== userAddress.toLowerCase()) {
|
|
249
|
+
addressesToProcess.push(recipientAddress.toLowerCase())
|
|
250
|
+
}
|
|
252
251
|
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
const relevantExpectedChanges = Object.entries(expectedStateChanges).flatMap(
|
|
253
|
+
([address, changes]) =>
|
|
254
|
+
addressesToProcess.includes(address.toLowerCase()) ? changes ?? [] : []
|
|
255
|
+
)
|
|
255
256
|
|
|
256
|
-
if (!simulationResultHasAssets(
|
|
257
|
+
if (!simulationResultHasAssets(relevantExpectedChanges)) {
|
|
257
258
|
return successDefaultResult
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
|
|
261
|
+
relevantExpectedChanges.forEach((expectedChange) => {
|
|
261
262
|
handleExpectedBalanceChange(expectedChange, simulationResult, { network })
|
|
262
263
|
})
|
|
263
264
|
|