@exodus/ethereum-api 9.0.0-alpha.2 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2121 -0
- package/LICENSE +7 -0
- package/README.md +124 -0
- package/package.json +34 -33
- package/src/address-has-history.js +6 -2
- package/src/allowance/constants.js +4 -5
- package/src/allowance/index.js +159 -40
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +104 -0
- package/src/check-tx/index.js +1 -0
- package/src/create-asset-plugin-factory.js +115 -0
- package/src/create-asset-utils.js +520 -0
- package/src/create-asset.js +272 -97
- package/src/create-token-factory.js +47 -17
- package/src/custom-fees.js +57 -0
- package/src/eip7702-whitelist.js +62 -0
- package/src/ens/index.js +12 -17
- package/src/error-wrapper.js +383 -0
- package/src/eth-like-util.js +172 -33
- package/src/exodus-eth-server/api-coin-nodes.js +27 -196
- package/src/exodus-eth-server/api.js +23 -14
- package/src/exodus-eth-server/clarity-v2.js +255 -0
- package/src/exodus-eth-server/clarity.js +67 -265
- package/src/exodus-eth-server/errors.js +40 -0
- package/src/exodus-eth-server/eth-like-server.js +385 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/exodus-eth-server/index.js +77 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/exodus-eth-server/ws-gateway.js +306 -0
- package/src/exodus-eth-server/ws.js +1 -2
- package/src/fee-data/EthLikeFeeData.js +81 -0
- package/src/fee-data/index.js +54 -0
- package/src/fee-utils.js +229 -0
- package/src/gas-estimation.js +183 -66
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +285 -27
- package/src/get-fee.js +92 -56
- package/src/get-historical-balance.js +58 -0
- package/src/hooks/index.js +1 -1
- package/src/hooks/monitor.js +182 -43
- package/src/hooks/staking-freshness.js +58 -0
- package/src/index.js +118 -12
- package/src/move-funds.js +153 -0
- package/src/multicall3/index.js +212 -0
- package/src/multisig-address.js +146 -0
- package/src/nft-utils.js +60 -0
- package/src/number-utils.js +34 -2
- package/src/optimism-gas/index.js +13 -5
- package/src/send-validations.js +58 -0
- package/src/server-based-fee-monitor.js +12 -19
- package/src/simulate-tx/fetch-tx-preview.js +0 -2
- package/src/simulate-tx/index.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +24 -11
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +108 -0
- package/src/simulation/estimate-fee.js +14 -0
- package/src/simulation/estimate-simple-transfer.js +15 -0
- package/src/simulation/get-message-type.js +18 -0
- package/src/simulation/simulate-message-api.js +68 -0
- package/src/simulation/simulate-transactions-api.js +266 -0
- package/src/simulation/simulate-transactions.js +16 -0
- package/src/simulation/transactions.js +52 -0
- package/src/simulation/try-estimating-changes-locally.js +26 -0
- package/src/staking/api/index.js +43 -0
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +166 -34
- package/src/staking/ethereum/deps.js +37 -0
- package/src/staking/ethereum/everstake.js +113 -0
- package/src/staking/ethereum/index.js +4 -3
- package/src/staking/ethereum/service.js +682 -181
- package/src/staking/ethereum/staking-utils.js +275 -4
- package/src/staking/fantom-staking.js +9 -9
- package/src/staking/index.js +4 -4
- package/src/staking/matic/addresses.js +2 -0
- package/src/staking/matic/api.js +69 -49
- package/src/staking/matic/index.js +3 -3
- package/src/staking/matic/matic-staking-utils.js +243 -5
- package/src/staking/matic/matic-staking.md +53 -0
- package/src/staking/staking-provider-client.js +1 -1
- package/src/staking/utils/index.js +39 -0
- package/src/tx-create.js +666 -0
- package/src/tx-log/clarity-monitor.js +273 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
- package/src/tx-log/clarity-utils/absolute.js +96 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
- package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/clarity-utils/index.js +9 -3
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
- package/src/tx-log/ethereum-monitor.js +93 -47
- package/src/tx-log/ethereum-no-history-monitor.js +52 -41
- package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
- package/src/tx-log/index.js +5 -3
- package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
- package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
- package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
- package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
- package/src/tx-log/monitor-utils/index.js +12 -7
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
- package/src/tx-log-staking-processor/index.js +28 -14
- package/src/tx-log-staking-processor/utils.js +21 -20
- package/src/tx-send/broadcast-error-handler.js +63 -0
- package/src/tx-send/index.js +1 -2
- package/src/tx-send/nonce-utils.js +97 -0
- package/src/tx-send/tx-send.js +94 -288
- package/src/tx-sign/index.js +17 -0
- package/src/tx-type/index.js +280 -0
- package/src/watch-tx.js +36 -0
- package/src/web3/createSimulateMessage.js +3 -2
- package/src/web3/createSimulateTransactions.js +11 -0
- package/src/web3/index.js +3 -1
- package/src/etherscan/account.js +0 -49
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -16
- package/src/etherscan/proxy.js +0 -47
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
- package/src/exodus-eth-server/server-factory.js +0 -28
- package/src/exodus-eth-server/server-map.js +0 -21
- package/src/get-fee-async.js +0 -97
- package/src/staking/matic/service.js +0 -437
- package/src/staking-api.js +0 -5
- package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
- package/src/tx-send/get-fee-info.js +0 -41
- package/src/websocket/index.android.js +0 -2
- package/src/websocket/index.ios.js +0 -2
- package/src/websocket/index.js +0 -21
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# `clarity-truncated-history` monitor
|
|
2
|
+
|
|
3
|
+
Extends `ClarityMonitor`. Used when Clarity only serves a **recent window** of txs and attaches pre-cursor snapshots in `accountInfo`.
|
|
4
|
+
|
|
5
|
+
- Sends `truncated=true` on `getAllTransactions`.
|
|
6
|
+
- Folds `accountInfo` into `accountState`, including truncated-history baselines.
|
|
7
|
+
- Forces `rpcBalanceAssetNames = []` (no per-asset RPC `eth_getBalance` batch).
|
|
8
|
+
|
|
9
|
+
Plugins opting in: `bsc-plugin`, `basemainnet-plugin`, `ethereum-plugin`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Flow
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TD
|
|
17
|
+
A[Clarity HTTP<br/>GET /addresses/:addr/transactions<br/>truncated=true & cursor] --> B[response]
|
|
18
|
+
B --> B1[transactions.confirmed + .pending]
|
|
19
|
+
B --> B2[accountInfo rows<br/>account_balance or truncated baseline]
|
|
20
|
+
B --> B3[cursor]
|
|
21
|
+
|
|
22
|
+
B1 --> C[normalizeTransactionsResponse<br/>filter spam + stale pending]
|
|
23
|
+
C --> D[getAllLogItemsByAsset<br/>bucket per assetName]
|
|
24
|
+
D --> E[aci.updateTxLogAndNotifyBatch<br/>per asset]
|
|
25
|
+
|
|
26
|
+
B2 --> F[extractBalanceFromClarityAccountInfo<br/>balance? tokenBalances?<br/>truncatedAccountState?]
|
|
27
|
+
|
|
28
|
+
E --> G[accountState]
|
|
29
|
+
F --> G
|
|
30
|
+
B3 --> G[accountState<br/>balance, tokenBalances,<br/>truncatedAccountState,<br/>clarityCursor]
|
|
31
|
+
|
|
32
|
+
G --> H1[getBalances - asset.api.getBalances]
|
|
33
|
+
G --> H2[resolveNonce - tx send path]
|
|
34
|
+
|
|
35
|
+
H1 --> I1{txLog empty?}
|
|
36
|
+
I1 -- yes --> I2[accountState.balance]
|
|
37
|
+
I1 -- no --> I3{accountState<br/>balance<br/>non-zero?}
|
|
38
|
+
I3 -- yes + token account balance --> I2
|
|
39
|
+
I3 -- no / native --> I4[walk txLog<br/>use truncated baseline only when no balanceChange exists]
|
|
40
|
+
|
|
41
|
+
H2 --> J1[max - nonceFromTxLog,<br/>accountState.nonce]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Left-to-right: `accountInfo` can write native balance baselines, authoritative token balances, and truncated baselines to `accountState`. Native ETH still prioritizes tx history when history exists. Rows with `source: 'account_balance'` go into `balance`/`tokenBalances`; rows with `source: 'truncated'` go into `truncatedAccountState`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Example response (trimmed)
|
|
49
|
+
|
|
50
|
+
Wallet `0x3F80…9164` on Ethereum. Two txs + the one real `accountInfo` row:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"transactions": [
|
|
55
|
+
{
|
|
56
|
+
"blockNumber": 24190233,
|
|
57
|
+
"hash": "0x7544…35bb",
|
|
58
|
+
"from": "0xccef06b2…8136c",
|
|
59
|
+
"to": "0xfa7093cd…fa4b9",
|
|
60
|
+
"effects": [
|
|
61
|
+
{
|
|
62
|
+
"address": "0xa0b86991…0eb48",
|
|
63
|
+
"effect": "erc20",
|
|
64
|
+
"from": "0xfa7093cd…fa4b9",
|
|
65
|
+
"to": "0x3f80…9164",
|
|
66
|
+
"value": "9668069"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"walletChanges": [
|
|
70
|
+
{
|
|
71
|
+
"wallet": "0x3f80…9164",
|
|
72
|
+
"type": "token",
|
|
73
|
+
"from": "0",
|
|
74
|
+
"to": "9668069",
|
|
75
|
+
"contract": "0xa0b86991…0eb48"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"blockNumber": 24204497,
|
|
81
|
+
"hash": "0xb5fa…5b23",
|
|
82
|
+
"from": "0x3f80…9164",
|
|
83
|
+
"to": "0x2a8e…5e86",
|
|
84
|
+
"nonce": "0",
|
|
85
|
+
"effects": [
|
|
86
|
+
{
|
|
87
|
+
"address": "0x2a8e…5e86",
|
|
88
|
+
"effect": "erc20",
|
|
89
|
+
"from": "0x3f80…9164",
|
|
90
|
+
"to": "0xcbad…4853",
|
|
91
|
+
"value": "10000000000000000000"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"walletChanges": [
|
|
95
|
+
{
|
|
96
|
+
"wallet": "0x3f80…9164",
|
|
97
|
+
"type": "balance",
|
|
98
|
+
"from": "254470008324222",
|
|
99
|
+
"to": "101067364839680"
|
|
100
|
+
},
|
|
101
|
+
{ "wallet": "0x3f80…9164", "type": "nonce", "from": "0", "to": "1" }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"cursor": 24211168,
|
|
106
|
+
"accountInfo": [
|
|
107
|
+
{
|
|
108
|
+
"type": "token",
|
|
109
|
+
"source": "account_balance",
|
|
110
|
+
"value": "20191469428776270391810",
|
|
111
|
+
"blockNumber": 24933420,
|
|
112
|
+
"assetId": "0x2a8e…5e86",
|
|
113
|
+
"assetName": "ousd_ethereum_48fcf72d",
|
|
114
|
+
"decimals": 18
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **Tx A** — incoming USDC (9.668069). Balance derivable from `walletChanges`.
|
|
121
|
+
- **Tx B** — first outgoing OUSD send, nonce `0 → 1`, ETH balance delta.
|
|
122
|
+
- **`accountInfo`** — OUSD rebase balance ≈ **20,191.469 OUSD**, authoritative.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## `accountState` after this tick
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
{
|
|
130
|
+
clarityCursor: <Buffer LE 24211168>,
|
|
131
|
+
tokenBalances: {
|
|
132
|
+
ousd_ethereum_48fcf72d: NumberUnit('20191.469… OUSD'), // account_balance row
|
|
133
|
+
},
|
|
134
|
+
truncatedAccountState: {
|
|
135
|
+
balance: ZERO,
|
|
136
|
+
tokenBalances: {},
|
|
137
|
+
},
|
|
138
|
+
balance: ZERO, // no native balance row in this response
|
|
139
|
+
nonce: 0, // unchanged by monitor; derived live by resolveNonce
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`accountInfo` **only** wrote `tokenBalances.ousd_...` here — no `type: 'balance'` or `type: 'nonce'` rows were sent. Everything else is derived at read time from the tx log.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Read paths
|
|
148
|
+
|
|
149
|
+
### `getBalances`
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
asset branch taken result
|
|
153
|
+
ethereum (ETH) walk txLog → latest 43_326_434_520_430 wei
|
|
154
|
+
balanceChange.to
|
|
155
|
+
usdc walk txLog → latest 95_948_474_200 base
|
|
156
|
+
walletChange.to
|
|
157
|
+
ousd_ethereum_48fcf72d accountState short-circuit 20_191_469_428_… base
|
|
158
|
+
(account_balance row) (from accountInfo)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Source: `getAbsoluteBalance` in `src/get-balances.js`, ordered branches:
|
|
162
|
+
|
|
163
|
+
1. Non-zero `accountState` balance → return it.
|
|
164
|
+
2. `txLog.size === 0` → return `truncatedAccountState` balance or `ZERO`.
|
|
165
|
+
3. Token value exists in `accountState.tokenBalances` → return accountState balance, including explicit zero.
|
|
166
|
+
4. Walk reversed `txLog` for canonical `balanceChange` tx.
|
|
167
|
+
5. If no absolute balance exists, fall back to the txLog-derived balance path.
|
|
168
|
+
|
|
169
|
+
### `resolveNonce` (`src/tx-send/nonce-utils.js`)
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
nonce = Math.max(
|
|
173
|
+
nonceFromTxLog, // latest canonical `type: 'nonce'` walletChange
|
|
174
|
+
accountState.nonce || 0 // any `accountInfo` `type: 'nonce'` row, regardless of source
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Both `account_balance` and `truncated` source nonce rows collapse into `accountState.nonce`
|
|
179
|
+
(the max wins) — they represent the same chain-known nonce, just at different observation
|
|
180
|
+
points. `accountInfo` `nonce` row patches the gap when truncation hides the latest
|
|
181
|
+
`nonceChange`.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Monitor + server wiring
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
// create-asset-utils.js
|
|
189
|
+
case 'clarity-truncated-history':
|
|
190
|
+
monitor = new ClarityTruncatedHistoryMonitor({ server, ...args }) // server is ClarityServerV2
|
|
191
|
+
|
|
192
|
+
// exodus-eth-server/index.js — no new server class
|
|
193
|
+
case 'clarity-v2':
|
|
194
|
+
case 'clarity-truncated-history': // normalized to clarity-v2
|
|
195
|
+
return new ClarityServerV2(...)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Backend contract
|
|
201
|
+
|
|
202
|
+
1. Accept `?truncated=true` on `GET /addresses/:addr/transactions`.
|
|
203
|
+
2. Attach `accountInfo: []` (or omit) when nothing to snapshot.
|
|
204
|
+
3. Emit `source: 'account_balance'` rows for current account/token balances. These may appear on every fetch.
|
|
205
|
+
4. Emit `source: 'truncated'` rows on the initial `cursor=0` fetch for state that existed before the returned history window.
|
|
206
|
+
5. Row shapes: `{ type: 'balance' | 'nonce' | 'token', source: 'account_balance' | 'truncated', value, blockNumber, assetId?, assetName?, decimals? }`. Sorted client-side by `blockNumber` desc; first value per type/assetId wins for `balance`/`token`. `nonce` rows are collapsed via `Math.max` regardless of source.
|
|
207
|
+
6. Unknown `assetId` → client drops the row.
|
|
208
|
+
7. `value` arrives as a decimal string. `null`, `undefined`, and empty-string `""` mean "no value" and the row is skipped.
|
|
209
|
+
|
|
210
|
+
If the backend can't emit `accountInfo`, the monitor degrades gracefully to `clarity-v2` behavior (tx-log walk only).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Tests
|
|
215
|
+
|
|
216
|
+
- `src/tx-log/__tests__/clarity-truncated-history-monitor.test.js` — `truncated: true`, no RPC balance batch, accountInfo → `balance`, token `tokenBalances`, and `truncatedAccountState`.
|
|
217
|
+
- `src/__tests__/create-asset-utils.test.js` — factory → `ClarityTruncatedHistoryMonitor`.
|
|
218
|
+
- `src/__tests__/get-balances.test.js` — accountState token short-circuit and truncated baseline cases.
|
|
219
|
+
- `src/__tests__/balances-model.test.js` — default-ZERO accountState still falls through (regression for `NumberUnit.isZero` getter).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
import ms from 'ms'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CANONICAL_ABSOLUTE_TX_SEARCH_DEPTH = ms('5m')
|
|
5
|
+
|
|
6
|
+
const ABSOLUTE_FIELD_NAME_BALANCE_CHANGE = 'balanceChange'
|
|
7
|
+
const ABSOLUTE_FIELD_NAME_NONCE_CHANGE = 'nonceChange'
|
|
8
|
+
|
|
9
|
+
const ABSOLUTE_BALANCE_FIELDS = new Set([
|
|
10
|
+
ABSOLUTE_FIELD_NAME_BALANCE_CHANGE,
|
|
11
|
+
ABSOLUTE_FIELD_NAME_NONCE_CHANGE,
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
// Defines whether a `Tx` is an absolute balance
|
|
15
|
+
// node that can be sorted in terms of transaction
|
|
16
|
+
// ordering.
|
|
17
|
+
//
|
|
18
|
+
const isCanonicalAbsoluteTx = (tx, fieldName) => {
|
|
19
|
+
assert(ABSOLUTE_BALANCE_FIELDS.has(fieldName), `unsupported fieldName, "${fieldName}"`)
|
|
20
|
+
|
|
21
|
+
return Boolean(
|
|
22
|
+
tx?.data?.[fieldName] &&
|
|
23
|
+
typeof tx.data.blockNumber === 'number' &&
|
|
24
|
+
typeof tx.data.transactionIndex === 'number'
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const isCanonicalAbsoluteBalanceTx = (tx) =>
|
|
29
|
+
isCanonicalAbsoluteTx(tx, ABSOLUTE_FIELD_NAME_BALANCE_CHANGE)
|
|
30
|
+
export const isCanonicalAbsoluteNonceTx = (tx) =>
|
|
31
|
+
isCanonicalAbsoluteTx(tx, ABSOLUTE_FIELD_NAME_NONCE_CHANGE)
|
|
32
|
+
|
|
33
|
+
// Attempts to find the most recent absolute Tx for a given
|
|
34
|
+
// `txLog`.
|
|
35
|
+
//
|
|
36
|
+
// NOTE: Returns `null` if nothing is found.
|
|
37
|
+
// NOTE: Uses `tx.data.blockNumber` and `tx.data.transactionIndex`,
|
|
38
|
+
// which are not guaranteed to exist in older transaction
|
|
39
|
+
// histories.
|
|
40
|
+
const getLatestCanonicalAbsoluteTx = ({
|
|
41
|
+
// Due to nondeterministic sorting for records in close
|
|
42
|
+
// proximity to one-another on high TPS chains, the
|
|
43
|
+
// order of the `reversedTxLog` cannot be trusted to be
|
|
44
|
+
// exactly precise since two consecutive blocks may share
|
|
45
|
+
// the same timestamp. Therefore, when finding an absolute
|
|
46
|
+
// balance node, we use the`searchDepthMs` to take into
|
|
47
|
+
// account extra nodes chronologically deeper than what we
|
|
48
|
+
// presume to be the latest to make sure we resolve to the
|
|
49
|
+
// correct node.
|
|
50
|
+
searchDepthMs = DEFAULT_CANONICAL_ABSOLUTE_TX_SEARCH_DEPTH,
|
|
51
|
+
reversedTxLog,
|
|
52
|
+
fieldName,
|
|
53
|
+
deadline = Infinity,
|
|
54
|
+
}) => {
|
|
55
|
+
assert(reversedTxLog, 'expected reversedTxLog')
|
|
56
|
+
|
|
57
|
+
let latest = null
|
|
58
|
+
|
|
59
|
+
for (const tx of reversedTxLog) {
|
|
60
|
+
if (+tx.date > deadline) continue
|
|
61
|
+
|
|
62
|
+
if (latest) {
|
|
63
|
+
const diff = +latest.date - +tx.date
|
|
64
|
+
|
|
65
|
+
if (diff >= searchDepthMs) break
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!isCanonicalAbsoluteTx(tx, fieldName)) continue
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
!latest ||
|
|
72
|
+
tx.data.blockNumber > latest.data.blockNumber ||
|
|
73
|
+
(tx.data.blockNumber === latest.data.blockNumber &&
|
|
74
|
+
tx.data.transactionIndex > latest.data.transactionIndex)
|
|
75
|
+
) {
|
|
76
|
+
latest = tx
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return latest
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const getLatestCanonicalAbsoluteBalanceTx = ({ searchDepthMs, reversedTxLog, deadline }) =>
|
|
84
|
+
getLatestCanonicalAbsoluteTx({
|
|
85
|
+
searchDepthMs,
|
|
86
|
+
reversedTxLog,
|
|
87
|
+
deadline,
|
|
88
|
+
fieldName: ABSOLUTE_FIELD_NAME_BALANCE_CHANGE,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export const getLatestCanonicalAbsoluteNonceTx = ({ searchDepthMs, reversedTxLog }) =>
|
|
92
|
+
getLatestCanonicalAbsoluteTx({
|
|
93
|
+
searchDepthMs,
|
|
94
|
+
reversedTxLog,
|
|
95
|
+
fieldName: ABSOLUTE_FIELD_NAME_NONCE_CHANGE,
|
|
96
|
+
})
|
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
import getValueOfTransfers from '../monitor-utils/get-value-of-transfers'
|
|
2
|
-
import getTransfersByTokenName from '../monitor-utils/get-transfers-by-token-name'
|
|
3
|
-
import getFeeAmount from '../monitor-utils/get-fee-amount'
|
|
4
|
-
import isConsideredSentTokenTx from '../monitor-utils/is-considered-sent-token-tx'
|
|
5
|
-
import isConfirmedServerTx from '../monitor-utils/is-confirmed-server-tx'
|
|
6
|
-
import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx'
|
|
7
|
-
import filterEffects from './filter-effects'
|
|
8
1
|
import lodash from 'lodash'
|
|
9
2
|
|
|
10
|
-
|
|
3
|
+
import { getWalletUpdates } from '../monitor-utils/get-balance-updates.js'
|
|
4
|
+
import getFeeAmount from '../monitor-utils/get-fee-amount.js'
|
|
5
|
+
import getRawTransferAmount from '../monitor-utils/get-raw-transfer-amount.js'
|
|
6
|
+
import getTransfersByTokenName from '../monitor-utils/get-transfers-by-token-name.js'
|
|
7
|
+
import getValueOfTransfers from '../monitor-utils/get-value-of-transfers.js'
|
|
8
|
+
import isConfirmedServerTx from '../monitor-utils/is-confirmed-server-tx.js'
|
|
9
|
+
import isConsideredSentTokenTx from '../monitor-utils/is-considered-sent-token-tx.js'
|
|
10
|
+
import filterEffects from './filter-effects.js'
|
|
11
|
+
import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx.js'
|
|
12
|
+
|
|
13
|
+
const isFiniteInteger = (n) => Number.isFinite(n) && Number.isInteger(n)
|
|
14
|
+
|
|
15
|
+
// This function takes a server transaction object fetched from clarity,
|
|
11
16
|
// and transforms it into Tx models to update the exodus state.
|
|
12
17
|
|
|
18
|
+
const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
|
|
19
|
+
transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
|
|
20
|
+
|
|
21
|
+
// If the timestamp is in the future, use the current time.
|
|
22
|
+
const parseServerTxDate = (timestamp) => new Date(Math.min(Date.now(), parseInt(timestamp, 16)))
|
|
23
|
+
|
|
24
|
+
function isSelfSendTx({
|
|
25
|
+
coinAmount,
|
|
26
|
+
ourWalletWasSender,
|
|
27
|
+
sendingTransferPresent,
|
|
28
|
+
receivingTransferPresent,
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
export default function getLogItemsFromServerTx({
|
|
14
36
|
serverTx,
|
|
15
37
|
asset,
|
|
@@ -22,6 +44,8 @@ export default function getLogItemsFromServerTx({
|
|
|
22
44
|
const txId = serverTx.hash
|
|
23
45
|
const nonce = parseInt(serverTx.nonce, 10)
|
|
24
46
|
const gasLimit = parseInt(serverTx.gas, 10)
|
|
47
|
+
const blockNumber = serverTx.blockNumber
|
|
48
|
+
const transactionIndex = parseInt(serverTx.transactionIndex, 10)
|
|
25
49
|
const error = serverTx.error || (serverTx.status === '0' ? 'Failed' : null)
|
|
26
50
|
const feeAmount = getFeeAmount(asset, serverTx)
|
|
27
51
|
const internalTransfers = filterEffects(serverTx.effects, 'internal') || []
|
|
@@ -34,6 +58,9 @@ export default function getLogItemsFromServerTx({
|
|
|
34
58
|
const methodId = serverTx.input && {
|
|
35
59
|
methodId: serverTx.input.slice(0, Math.max(0, METHOD_ID_LENGTH)),
|
|
36
60
|
}
|
|
61
|
+
const data = (ourWalletWasSender ? serverTx.input : methodId?.methodId) || '0x'
|
|
62
|
+
const walletUpdates = getWalletUpdates(ourWalletAddress, serverTx.walletChanges || [])
|
|
63
|
+
const { baseBalanceUpdate, nonceUpdate, tokenBalancesUpdate } = walletUpdates
|
|
37
64
|
|
|
38
65
|
const logItemCommonProperties = {
|
|
39
66
|
confirmations,
|
|
@@ -49,14 +76,41 @@ export default function getLogItemsFromServerTx({
|
|
|
49
76
|
const sendingTransferPresent = ethereumTransfers.some(({ from }) => from === ourWalletAddress)
|
|
50
77
|
const receivingTransferPresent = ethereumTransfers.some(({ to }) => to === ourWalletAddress)
|
|
51
78
|
|
|
52
|
-
|
|
79
|
+
const shouldAttachTx =
|
|
80
|
+
sendingTransferPresent || receivingTransferPresent || baseBalanceUpdate || nonceUpdate // need to also include if there's a base balance change
|
|
81
|
+
|
|
82
|
+
if (shouldAttachTx) {
|
|
53
83
|
const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
|
|
84
|
+
// `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
|
|
85
|
+
// and is consumed by the bump path (`tx-create.js`) to recover the
|
|
86
|
+
// user-signed on-chain `value` when accelerating a self-send (where
|
|
87
|
+
// `coinAmount` is zero). For the base-asset entry the raw amount is the
|
|
88
|
+
// top-level `value` field of the tx itself, not the sum of internal
|
|
89
|
+
// transfers — that's what the user signed and what we must restore on
|
|
90
|
+
// bump.
|
|
91
|
+
const rawCoinAmount = getRawTransferAmount({
|
|
92
|
+
ourWalletAddress,
|
|
93
|
+
asset,
|
|
94
|
+
transfers: [serverTx],
|
|
95
|
+
}).toBaseString()
|
|
54
96
|
const selfSend = isSelfSendTx({
|
|
55
97
|
coinAmount,
|
|
56
98
|
ourWalletWasSender,
|
|
57
99
|
sendingTransferPresent,
|
|
58
100
|
receivingTransferPresent,
|
|
59
101
|
})
|
|
102
|
+
let sent
|
|
103
|
+
if (sendingTransferPresent) {
|
|
104
|
+
sent = internalTransfers
|
|
105
|
+
.map(({ from, to, value }) => {
|
|
106
|
+
if (from === ourWalletAddress) {
|
|
107
|
+
const amount = asset.currency.baseUnit(value).toDefaultString({ unit: true })
|
|
108
|
+
return { address: to, amount }
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
}
|
|
113
|
+
|
|
60
114
|
logItemsForServerTxEntries.push([
|
|
61
115
|
asset.name,
|
|
62
116
|
{
|
|
@@ -64,10 +118,16 @@ export default function getLogItemsFromServerTx({
|
|
|
64
118
|
coinAmount,
|
|
65
119
|
coinName: asset.name,
|
|
66
120
|
data: {
|
|
67
|
-
data
|
|
121
|
+
data,
|
|
68
122
|
nonce,
|
|
69
123
|
gasLimit,
|
|
124
|
+
rawCoinAmount,
|
|
125
|
+
balanceChange: baseBalanceUpdate,
|
|
126
|
+
nonceChange: nonceUpdate,
|
|
70
127
|
...methodId,
|
|
128
|
+
...(sent?.length > 0 ? { sent } : undefined),
|
|
129
|
+
...(isFiniteInteger(blockNumber) ? { blockNumber } : null),
|
|
130
|
+
...(isFiniteInteger(transactionIndex) ? { transactionIndex } : null),
|
|
71
131
|
},
|
|
72
132
|
...(ourWalletWasSender
|
|
73
133
|
? { from: [], to: toAddress, feeAmount, feeCoinName: asset.feeAsset.name }
|
|
@@ -94,12 +154,23 @@ export default function getLogItemsFromServerTx({
|
|
|
94
154
|
Object.entries(tokenTransfersByTokenName).forEach(([tokenName, tokenTransfers]) => {
|
|
95
155
|
const sendingTransferPresent = tokenTransfers.some(({ from }) => from === ourWalletAddress)
|
|
96
156
|
const receivingTransferPresent = tokenTransfers.some(({ to }) => to === ourWalletAddress)
|
|
97
|
-
if (!sendingTransferPresent && !receivingTransferPresent) return
|
|
98
|
-
|
|
99
157
|
const token = assets[tokenName]
|
|
100
|
-
const
|
|
158
|
+
const tokenAddress = token.addresses.current.toLowerCase()
|
|
159
|
+
const balanceChange = tokenBalancesUpdate[tokenAddress]
|
|
160
|
+
|
|
161
|
+
if (!sendingTransferPresent && !receivingTransferPresent && !balanceChange) return
|
|
101
162
|
|
|
163
|
+
const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
|
|
102
164
|
const coinAmount = getValueOfTransfers(ourWalletAddress, token, tokenTransfers)
|
|
165
|
+
// See base-entry comment above; for token entries the raw amount is the
|
|
166
|
+
// absolute total tokens our wallet moved in this tx (sum of outgoing, or
|
|
167
|
+
// incoming for receive-only entries).
|
|
168
|
+
const rawCoinAmount = getRawTransferAmount({
|
|
169
|
+
ourWalletAddress,
|
|
170
|
+
asset: token,
|
|
171
|
+
transfers: tokenTransfers,
|
|
172
|
+
}).toBaseString()
|
|
173
|
+
|
|
103
174
|
const tokenFromAddresses = lodash.uniq(
|
|
104
175
|
tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
|
|
105
176
|
)
|
|
@@ -123,7 +194,16 @@ export default function getLogItemsFromServerTx({
|
|
|
123
194
|
...logItemCommonProperties,
|
|
124
195
|
coinAmount,
|
|
125
196
|
coinName: tokenName,
|
|
126
|
-
data: {
|
|
197
|
+
data: {
|
|
198
|
+
data,
|
|
199
|
+
nonce,
|
|
200
|
+
gasLimit,
|
|
201
|
+
rawCoinAmount,
|
|
202
|
+
balanceChange,
|
|
203
|
+
...methodId,
|
|
204
|
+
...(isFiniteInteger(blockNumber) ? { blockNumber } : null),
|
|
205
|
+
...(isFiniteInteger(transactionIndex) ? { transactionIndex } : null),
|
|
206
|
+
},
|
|
127
207
|
...(isConsideredSent
|
|
128
208
|
? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: token.feeAsset.name }
|
|
129
209
|
: { from: tokenFromAddresses }),
|
|
@@ -138,20 +218,3 @@ export default function getLogItemsFromServerTx({
|
|
|
138
218
|
|
|
139
219
|
return Object.fromEntries(logItemsForServerTxEntries)
|
|
140
220
|
}
|
|
141
|
-
|
|
142
|
-
const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
|
|
143
|
-
transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
|
|
144
|
-
|
|
145
|
-
// If the timestamp is in the future, use the current time.
|
|
146
|
-
const parseServerTxDate = (timestamp) => new Date(Math.min(Date.now(), parseInt(timestamp, 16)))
|
|
147
|
-
|
|
148
|
-
function isSelfSendTx({
|
|
149
|
-
coinAmount,
|
|
150
|
-
ourWalletWasSender,
|
|
151
|
-
sendingTransferPresent,
|
|
152
|
-
receivingTransferPresent,
|
|
153
|
-
}) {
|
|
154
|
-
return (
|
|
155
|
-
coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
|
|
156
|
-
)
|
|
157
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import lodash from 'lodash'
|
|
2
|
-
import getValueOfTransfers from '../monitor-utils/get-value-of-transfers'
|
|
3
1
|
import { FEE_PAYMENT_PREFIX } from '@exodus/ethereum-lib'
|
|
2
|
+
import lodash from 'lodash'
|
|
3
|
+
|
|
4
|
+
import getValueOfTransfers from '../monitor-utils/get-value-of-transfers.js'
|
|
4
5
|
|
|
5
6
|
// For ETH transactions, we store an array of token names inside
|
|
6
7
|
// the TX model. This array is used to display text in the UI for
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
export { default as
|
|
2
|
-
export { default as
|
|
3
|
-
export {
|
|
1
|
+
export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx.js'
|
|
2
|
+
export { default as filterEffects } from './filter-effects.js'
|
|
3
|
+
export {
|
|
4
|
+
isCanonicalAbsoluteBalanceTx,
|
|
5
|
+
isCanonicalAbsoluteNonceTx,
|
|
6
|
+
getLatestCanonicalAbsoluteBalanceTx,
|
|
7
|
+
getLatestCanonicalAbsoluteNonceTx,
|
|
8
|
+
} from './absolute.js'
|
|
9
|
+
export { normalizeTransactionsResponse } from './normalize-transactions-response.js'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const isPendingClarityTx = (tx) => tx?.blockNumber == null
|
|
2
|
+
|
|
3
|
+
const isSpamNote = (note) => {
|
|
4
|
+
if (!note || typeof note !== 'object') return false
|
|
5
|
+
if (typeof note.type !== 'string') return false
|
|
6
|
+
return note.type.toLowerCase() === 'spam'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const isSpamPendingTx = (tx) => {
|
|
10
|
+
if (!isPendingClarityTx(tx)) return false
|
|
11
|
+
if (!Array.isArray(tx?.transactionNotes)) return false
|
|
12
|
+
return tx.transactionNotes.some((note) => isSpamNote(note))
|
|
13
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import { isPendingClarityTx, isSpamPendingTx } from './is-spam-pending-tx.js'
|
|
4
|
+
|
|
5
|
+
// Converts the `pending` and `confirmed` transactions returned
|
|
6
|
+
// by Clarity into a single contiguous array of transactions.
|
|
7
|
+
//
|
|
8
|
+
// Since it is possible for Clarity to return very old pending
|
|
9
|
+
// transactions that our own RPC has forgotten (see `--txpool.lifetime`),
|
|
10
|
+
// we ensure that all transactions we expose to the consumer have
|
|
11
|
+
// `nonce`s do not exceed the maximum pending nonce that's currently
|
|
12
|
+
// maintained at the RPC.
|
|
13
|
+
//
|
|
14
|
+
// This avoids the "future transaction replaces pending" error.
|
|
15
|
+
export const normalizeTransactionsResponse = async ({
|
|
16
|
+
asset,
|
|
17
|
+
fromAddress,
|
|
18
|
+
response,
|
|
19
|
+
walletAccount,
|
|
20
|
+
}) => {
|
|
21
|
+
assert(asset, 'expected asset')
|
|
22
|
+
assert(fromAddress, 'expected fromAddress')
|
|
23
|
+
assert(response, 'expected response')
|
|
24
|
+
assert(walletAccount, 'expected walletAccount')
|
|
25
|
+
|
|
26
|
+
const { baseAsset } = asset
|
|
27
|
+
|
|
28
|
+
// NOTE: We query the current pending `nonce` from the RPC
|
|
29
|
+
// to sanity check the contents of the `txLog`.
|
|
30
|
+
const pendingNonce = await baseAsset
|
|
31
|
+
.getNonce({
|
|
32
|
+
asset: baseAsset,
|
|
33
|
+
fromAddress,
|
|
34
|
+
walletAccount,
|
|
35
|
+
forceFromNode: true,
|
|
36
|
+
})
|
|
37
|
+
.catch(() => null)
|
|
38
|
+
|
|
39
|
+
const allTxs = [...response.transactions.pending, ...response.transactions.confirmed].filter(
|
|
40
|
+
(tx) => {
|
|
41
|
+
// Pending spam transactions should not enter txlog state at all.
|
|
42
|
+
if (isSpamPendingTx(tx)) return false
|
|
43
|
+
|
|
44
|
+
// If the transaction isn't one we've sent, then ignore.
|
|
45
|
+
if (tx.from?.toLowerCase() !== fromAddress.toLowerCase()) return true
|
|
46
|
+
|
|
47
|
+
const nonce = parseInt(tx.nonce, 10)
|
|
48
|
+
if (!Number.isFinite(nonce) || !Number.isInteger(nonce)) return false
|
|
49
|
+
|
|
50
|
+
// HACK: If we were unable to determine the `pendingNonce`, then
|
|
51
|
+
// allow send transactions through by default without
|
|
52
|
+
// further validation.
|
|
53
|
+
// NOTE: If the transaction is already confirmed, ignore RPC pending
|
|
54
|
+
// state comparison check since the RPC might be out-of-sync.
|
|
55
|
+
if (pendingNonce === null || !isPendingClarityTx(tx)) return true
|
|
56
|
+
|
|
57
|
+
// NOTE: If the pending `tx` has a nonce that's greater than
|
|
58
|
+
// what's considered pending at the RPC, it is no longer
|
|
59
|
+
// viable and should be replaced.
|
|
60
|
+
return nonce < pendingNonce
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return { allTxs }
|
|
65
|
+
}
|