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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +2139 -0
  2. package/LICENSE +7 -0
  3. package/README.md +124 -0
  4. package/package.json +34 -33
  5. package/src/address-has-history.js +6 -2
  6. package/src/allowance/constants.js +4 -5
  7. package/src/allowance/index.js +159 -40
  8. package/src/check-tx/create-assess-transaction.js +23 -0
  9. package/src/check-tx/create-check-tx.js +104 -0
  10. package/src/check-tx/index.js +1 -0
  11. package/src/create-asset-plugin-factory.js +115 -0
  12. package/src/create-asset-utils.js +520 -0
  13. package/src/create-asset.js +273 -97
  14. package/src/create-token-factory.js +47 -17
  15. package/src/custom-fees.js +57 -0
  16. package/src/eip7702-whitelist.js +62 -0
  17. package/src/ens/index.js +12 -17
  18. package/src/error-wrapper.js +383 -0
  19. package/src/eth-like-util.js +172 -33
  20. package/src/exodus-eth-server/api-coin-nodes.js +27 -196
  21. package/src/exodus-eth-server/api.js +23 -14
  22. package/src/exodus-eth-server/clarity-v2.js +255 -0
  23. package/src/exodus-eth-server/clarity.js +67 -265
  24. package/src/exodus-eth-server/errors.js +40 -0
  25. package/src/exodus-eth-server/eth-like-server.js +385 -0
  26. package/src/exodus-eth-server/fetch-json.js +48 -0
  27. package/src/exodus-eth-server/index.js +77 -0
  28. package/src/exodus-eth-server/utils.js +31 -0
  29. package/src/exodus-eth-server/ws-gateway.js +306 -0
  30. package/src/exodus-eth-server/ws.js +1 -2
  31. package/src/fee-data/EthLikeFeeData.js +81 -0
  32. package/src/fee-data/index.js +54 -0
  33. package/src/fee-utils.js +229 -0
  34. package/src/gas-estimation.js +183 -66
  35. package/src/get-balance-for-address.js +1 -1
  36. package/src/get-balances.js +285 -27
  37. package/src/get-fee.js +92 -56
  38. package/src/get-historical-balance.js +58 -0
  39. package/src/hooks/index.js +1 -1
  40. package/src/hooks/monitor.js +182 -43
  41. package/src/hooks/staking-freshness.js +58 -0
  42. package/src/index.js +118 -12
  43. package/src/move-funds.js +153 -0
  44. package/src/multicall3/index.js +212 -0
  45. package/src/multisig-address.js +146 -0
  46. package/src/nft-utils.js +60 -0
  47. package/src/number-utils.js +34 -2
  48. package/src/optimism-gas/index.js +13 -5
  49. package/src/send-validations.js +58 -0
  50. package/src/server-based-fee-monitor.js +12 -19
  51. package/src/simulate-tx/fetch-tx-preview.js +0 -2
  52. package/src/simulate-tx/index.js +2 -2
  53. package/src/simulate-tx/simulate-eth-tx.js +24 -11
  54. package/src/simulation/common.js +34 -0
  55. package/src/simulation/create-simulate-message.js +49 -0
  56. package/src/simulation/create-simulate-transactions.js +108 -0
  57. package/src/simulation/estimate-fee.js +14 -0
  58. package/src/simulation/estimate-simple-transfer.js +15 -0
  59. package/src/simulation/get-message-type.js +18 -0
  60. package/src/simulation/simulate-message-api.js +68 -0
  61. package/src/simulation/simulate-transactions-api.js +266 -0
  62. package/src/simulation/simulate-transactions.js +16 -0
  63. package/src/simulation/transactions.js +52 -0
  64. package/src/simulation/try-estimating-changes-locally.js +26 -0
  65. package/src/staking/api/index.js +43 -0
  66. package/src/staking/cached-read.js +33 -0
  67. package/src/staking/ethereum/api.js +166 -34
  68. package/src/staking/ethereum/deps.js +37 -0
  69. package/src/staking/ethereum/everstake.js +113 -0
  70. package/src/staking/ethereum/index.js +4 -3
  71. package/src/staking/ethereum/service.js +682 -181
  72. package/src/staking/ethereum/staking-utils.js +275 -4
  73. package/src/staking/fantom-staking.js +9 -9
  74. package/src/staking/index.js +4 -4
  75. package/src/staking/matic/addresses.js +2 -0
  76. package/src/staking/matic/api.js +69 -49
  77. package/src/staking/matic/index.js +3 -3
  78. package/src/staking/matic/matic-staking-utils.js +243 -5
  79. package/src/staking/matic/matic-staking.md +53 -0
  80. package/src/staking/staking-provider-client.js +1 -1
  81. package/src/staking/utils/index.js +39 -0
  82. package/src/tx-create.js +666 -0
  83. package/src/tx-log/clarity-monitor.js +276 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +130 -0
  85. package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
  86. package/src/tx-log/clarity-utils/absolute.js +96 -0
  87. package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
  88. package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  89. package/src/tx-log/clarity-utils/index.js +9 -3
  90. package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
  91. package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
  92. package/src/tx-log/ethereum-monitor.js +93 -47
  93. package/src/tx-log/ethereum-no-history-monitor.js +52 -41
  94. package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
  95. package/src/tx-log/index.js +5 -3
  96. package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
  97. package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
  98. package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
  99. package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
  100. package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
  101. package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
  102. package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
  103. package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
  104. package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
  105. package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
  106. package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
  107. package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
  108. package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
  109. package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
  110. package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
  111. package/src/tx-log/monitor-utils/index.js +12 -7
  112. package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
  113. package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +90 -0
  114. package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
  115. package/src/tx-log-staking-processor/index.js +28 -14
  116. package/src/tx-log-staking-processor/utils.js +21 -20
  117. package/src/tx-send/broadcast-error-handler.js +63 -0
  118. package/src/tx-send/index.js +1 -2
  119. package/src/tx-send/nonce-utils.js +97 -0
  120. package/src/tx-send/tx-send.js +94 -288
  121. package/src/tx-sign/index.js +17 -0
  122. package/src/tx-type/index.js +280 -0
  123. package/src/watch-tx.js +36 -0
  124. package/src/web3/createSimulateMessage.js +3 -2
  125. package/src/web3/createSimulateTransactions.js +11 -0
  126. package/src/web3/index.js +3 -1
  127. package/src/etherscan/account.js +0 -49
  128. package/src/etherscan/index.js +0 -28
  129. package/src/etherscan/logs.js +0 -16
  130. package/src/etherscan/proxy.js +0 -47
  131. package/src/etherscan/request.js +0 -26
  132. package/src/etherscan/ws.js +0 -88
  133. package/src/exodus-eth-server/server-factory.js +0 -28
  134. package/src/exodus-eth-server/server-map.js +0 -21
  135. package/src/get-fee-async.js +0 -97
  136. package/src/staking/matic/service.js +0 -437
  137. package/src/staking-api.js +0 -5
  138. package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
  139. package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
  140. package/src/tx-send/get-fee-info.js +0 -41
  141. package/src/websocket/index.android.js +0 -2
  142. package/src/websocket/index.ios.js +0 -2
  143. package/src/websocket/index.js +0 -21
@@ -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
- // This function takes a server transaction object fetched from magnifier,
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
- if (sendingTransferPresent || receivingTransferPresent) {
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: serverTx.input || '0x',
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 tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
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: { nonce, gasLimit },
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 getDeriveDataNeededForTick } from './get-derive-data-needed-for-tick'
2
- export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx'
3
- export { default as filterEffects } from './filter-effects'
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
+ }