@exodus/ethereum-api 8.76.7 → 8.77.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.
- package/CHANGELOG.md +25 -0
- package/package.json +2 -2
- package/src/create-asset.js +20 -0
- package/src/create-token-factory.js +3 -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/get-historical-balance.js +58 -0
- package/src/index.js +2 -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/src/tx-log/clarity-utils/absolute.js +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
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.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.0...@exodus/ethereum-api@8.77.1) (2026-07-09)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add contract `staticcall` helper for `rpcRequestAccumulator` ([#8336](https://github.com/ExodusMovement/assets/issues/8336)) ([674f1a3](https://github.com/ExodusMovement/assets/commit/674f1a39c0a8fd5353b6ae7a19b79166f5aabfc9))
|
|
12
|
+
* 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))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **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))
|
|
18
|
+
* lower ETH min staking amount ([#8319](https://github.com/ExodusMovement/assets/issues/8319)) ([461e4c4](https://github.com/ExodusMovement/assets/commit/461e4c4f46e58e3750363edb9d0b6cddbfae5478))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [8.77.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.7...@exodus/ethereum-api@8.77.0) (2026-06-29)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* **ethereum:** implement getHistoricalBalance via clarity absolute balance ([#8281](https://github.com/ExodusMovement/assets/issues/8281)) ([be202a9](https://github.com/ExodusMovement/assets/commit/be202a94d9734d7af8ed118d11d4cc38c3b5ef10))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
6
31
|
## [8.76.7](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.6...@exodus/ethereum-api@8.76.7) (2026-06-26)
|
|
7
32
|
|
|
8
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.77.1",
|
|
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",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"type": "git",
|
|
70
70
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "96337ab20f90e6fa96043f7d569a4f6527c04ebd"
|
|
73
73
|
}
|
package/src/create-asset.js
CHANGED
|
@@ -41,7 +41,9 @@ import { createFeeData } from './fee-data/index.js'
|
|
|
41
41
|
import { createGetBalanceForAddress } from './get-balance-for-address.js'
|
|
42
42
|
import { getBalancesFactory } from './get-balances.js'
|
|
43
43
|
import { getFeeFactory } from './get-fee.js'
|
|
44
|
+
import { getHistoricalBalanceFactory } from './get-historical-balance.js'
|
|
44
45
|
import { moveFundsFactory } from './move-funds.js'
|
|
46
|
+
import { createEncodeMultisigContract } from './multisig-address.js'
|
|
45
47
|
import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
|
|
46
48
|
import { createSendValidations } from './send-validations.js'
|
|
47
49
|
import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
|
|
@@ -171,18 +173,32 @@ export const createAssetFactory = ({
|
|
|
171
173
|
encodePublic: encodePublicFactory({ chainId, useEip1191ChainIdChecksum }),
|
|
172
174
|
}
|
|
173
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
|
+
|
|
174
182
|
const getBalances = getBalancesFactory({
|
|
175
183
|
monitorType,
|
|
176
184
|
useAbsoluteBalance: useAbsoluteBalanceAndNonce,
|
|
177
185
|
rpcBalanceAssetNames,
|
|
178
186
|
})
|
|
179
187
|
|
|
188
|
+
const historicalBalanceSupported = monitorType !== 'no-history'
|
|
189
|
+
const getHistoricalBalance = getHistoricalBalanceFactory({
|
|
190
|
+
assetClientInterface,
|
|
191
|
+
getBalances,
|
|
192
|
+
})
|
|
193
|
+
|
|
180
194
|
const { createToken, getTokens } = createTokenFactory(
|
|
181
195
|
{
|
|
182
196
|
address,
|
|
183
197
|
bip44,
|
|
184
198
|
keys,
|
|
185
199
|
getBalances,
|
|
200
|
+
getHistoricalBalance,
|
|
201
|
+
historicalBalance: historicalBalanceSupported,
|
|
186
202
|
assetClientInterface,
|
|
187
203
|
server,
|
|
188
204
|
stakingConfiguration,
|
|
@@ -225,9 +241,11 @@ export const createAssetFactory = ({
|
|
|
225
241
|
family: ASSET_FAMILY.EVM,
|
|
226
242
|
feeMonitor: true,
|
|
227
243
|
feesApi: true,
|
|
244
|
+
historicalBalance: historicalBalanceSupported,
|
|
228
245
|
isMaxFeeAsset,
|
|
229
246
|
isTestnet,
|
|
230
247
|
moveFunds: true,
|
|
248
|
+
multisig: Boolean(encodeMultisigContract),
|
|
231
249
|
nfts,
|
|
232
250
|
noHistory: monitorType === 'no-history',
|
|
233
251
|
signWithSigner: true,
|
|
@@ -336,6 +354,7 @@ export const createAssetFactory = ({
|
|
|
336
354
|
features,
|
|
337
355
|
getActivityTxs,
|
|
338
356
|
getBalances,
|
|
357
|
+
...(features.historicalBalance && { getHistoricalBalance }),
|
|
339
358
|
getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
|
|
340
359
|
...(getBlackListStatus && { getBlackListStatus }),
|
|
341
360
|
getConfirmationsNumber: () => confirmationsNumber,
|
|
@@ -388,6 +407,7 @@ export const createAssetFactory = ({
|
|
|
388
407
|
address,
|
|
389
408
|
api,
|
|
390
409
|
chainId,
|
|
410
|
+
...(encodeMultisigContract && { encodeMultisigContract }),
|
|
391
411
|
monitorType,
|
|
392
412
|
estimateL1DataFee,
|
|
393
413
|
forceGasLimitEstimation,
|
|
@@ -37,6 +37,8 @@ const getActivityTxs = ({ txs }) => txs.filter((tx) => !smallbalanceTx(tx))
|
|
|
37
37
|
const getCreateBaseToken =
|
|
38
38
|
({
|
|
39
39
|
getBalances,
|
|
40
|
+
getHistoricalBalance,
|
|
41
|
+
historicalBalance,
|
|
40
42
|
assetClientInterface,
|
|
41
43
|
server,
|
|
42
44
|
stakingConfiguration = Object.create(null),
|
|
@@ -53,6 +55,7 @@ const getCreateBaseToken =
|
|
|
53
55
|
features: tokenFeatures,
|
|
54
56
|
hasFeature: (feature) => !!tokenFeatures[feature], // @deprecated use api.features instead
|
|
55
57
|
getBalances,
|
|
58
|
+
...(historicalBalance && { getHistoricalBalance }),
|
|
56
59
|
getTxLogFilter: name === 'polygon' ? getPolygonTxLogFilter : getTxLogFilter,
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TxSet } from '@exodus/models'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
import { getLatestCanonicalAbsoluteBalanceTx } from './tx-log/clarity-utils/index.js'
|
|
5
|
+
|
|
6
|
+
const walkBackFromBalance = ({ txLog, deadline, balance }) => {
|
|
7
|
+
let result = balance
|
|
8
|
+
|
|
9
|
+
for (const tx of txLog) {
|
|
10
|
+
if (new Date(tx.date).getTime() <= deadline) continue
|
|
11
|
+
if (tx.failed || tx.dropped || tx.pending || tx.data?.replacedBy) continue
|
|
12
|
+
|
|
13
|
+
result = result.sub(tx.coinAmount)
|
|
14
|
+
if (tx.feeAmount?.unitType.equals(tx.coinAmount.unitType)) {
|
|
15
|
+
result = result.add(tx.feeAmount)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return result.clampLowerZero()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds the Ethereum-like balance as of `date`. Anchors on the latest Clarity
|
|
24
|
+
* absolute-balance snapshot at or before the date, falling back to a walk-back
|
|
25
|
+
* from the current balance when no snapshot is available.
|
|
26
|
+
*/
|
|
27
|
+
export const getHistoricalBalanceFactory = ({ assetClientInterface, getBalances }) => {
|
|
28
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
29
|
+
assert(getBalances, 'getBalances is required')
|
|
30
|
+
|
|
31
|
+
return async ({ asset, walletAccount, date }) => {
|
|
32
|
+
assert(asset, 'asset is required')
|
|
33
|
+
assert(walletAccount, 'walletAccount is required')
|
|
34
|
+
assert(date instanceof Date, 'date is required')
|
|
35
|
+
|
|
36
|
+
const txLog = await assetClientInterface.getTxLog({ assetName: asset.name, walletAccount })
|
|
37
|
+
const deadline = date.getTime()
|
|
38
|
+
const anchor = getLatestCanonicalAbsoluteBalanceTx({ reversedTxLog: txLog.reverse(), deadline })
|
|
39
|
+
|
|
40
|
+
if (anchor) {
|
|
41
|
+
const total = asset.currency.baseUnit(anchor.data.balanceChange.to)
|
|
42
|
+
return { total, entries: [{ amount: total, date: deadline }] }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const accountState = await assetClientInterface.getAccountState({
|
|
46
|
+
assetName: asset.baseAsset.name,
|
|
47
|
+
walletAccount,
|
|
48
|
+
})
|
|
49
|
+
const confirmedTxLog = TxSet.fromArray([...txLog].filter((tx) => !tx.pending))
|
|
50
|
+
const balance = getBalances({ asset, accountState, txLog: confirmedTxLog }).spendable
|
|
51
|
+
const total = walkBackFromBalance({ txLog: confirmedTxLog, deadline, balance })
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
total,
|
|
55
|
+
entries: [{ amount: total, date: deadline }],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,8 @@ export {
|
|
|
38
38
|
} from './tx-log/index.js'
|
|
39
39
|
|
|
40
40
|
export { getStakingHistoryBalance, getBalancesFactory } from './get-balances.js'
|
|
41
|
+
export { getHistoricalBalanceFactory } from './get-historical-balance.js'
|
|
42
|
+
export { createEncodeMultisigContract } from './multisig-address.js'
|
|
41
43
|
|
|
42
44
|
export {
|
|
43
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
|
|
|
@@ -50,12 +50,15 @@ const getLatestCanonicalAbsoluteTx = ({
|
|
|
50
50
|
searchDepthMs = DEFAULT_CANONICAL_ABSOLUTE_TX_SEARCH_DEPTH,
|
|
51
51
|
reversedTxLog,
|
|
52
52
|
fieldName,
|
|
53
|
+
deadline = Infinity,
|
|
53
54
|
}) => {
|
|
54
55
|
assert(reversedTxLog, 'expected reversedTxLog')
|
|
55
56
|
|
|
56
57
|
let latest = null
|
|
57
58
|
|
|
58
59
|
for (const tx of reversedTxLog) {
|
|
60
|
+
if (+tx.date > deadline) continue
|
|
61
|
+
|
|
59
62
|
if (latest) {
|
|
60
63
|
const diff = +latest.date - +tx.date
|
|
61
64
|
|
|
@@ -77,10 +80,11 @@ const getLatestCanonicalAbsoluteTx = ({
|
|
|
77
80
|
return latest
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
export const getLatestCanonicalAbsoluteBalanceTx = ({ searchDepthMs, reversedTxLog }) =>
|
|
83
|
+
export const getLatestCanonicalAbsoluteBalanceTx = ({ searchDepthMs, reversedTxLog, deadline }) =>
|
|
81
84
|
getLatestCanonicalAbsoluteTx({
|
|
82
85
|
searchDepthMs,
|
|
83
86
|
reversedTxLog,
|
|
87
|
+
deadline,
|
|
84
88
|
fieldName: ABSOLUTE_FIELD_NAME_BALANCE_CHANGE,
|
|
85
89
|
})
|
|
86
90
|
|