@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +2121 -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 +272 -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 +273 -103
  84. package/src/tx-log/clarity-truncated-history-monitor.js +109 -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
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2024 Exodus Movement, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @exodus/ethereum-api
2
+
3
+ Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains. See [Asset Packages](../../docs/asset-packages.md) for more detail on this package`s role.
4
+
5
+ ## Fees
6
+
7
+ This section explains how fees currently work in Exodus and provides guidance on troubleshooting and tuning them via remote configuration. It reverse-engineers the existing unified implementation. The document aims to help readers understand the fee mechanism and suggest ways to improve it further.
8
+
9
+ ### Gas Price
10
+
11
+ Excluding custom fees, `feeData.gasPrice` is used to resolve the transaction's gas price (legacy) and max gas price (EIP-1559).
12
+
13
+ The `gasPrice` value is, in preference order, loaded from:
14
+
15
+ 1. Remote config
16
+ 2. The node via the fee data monitor and web socket. `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
17
+ 3. The default configuration.
18
+
19
+ Using remote config is never recommended, as manually updating this value won't keep up with network activity.
20
+
21
+ Fee and remote monitors are executed as soon as the wallet starts. The default fee data fields defined in code would be overridden (if they are set) before the user can interact with the wallet.
22
+
23
+ To tune the fee resolution, we can:
24
+
25
+ - Increase or decrease `gasPriceMultiplier` via remote config. For example, `1.2` would bump the current network gas price by 20%.
26
+ - Set a `min` value via remote config. Gas price will never be lower than this value.
27
+ - Set a `max` value via remote config. Gas price will never be higher than this value.
28
+
29
+ If the resolved `gasPrice` is not accurate or lower than the network's, the transaction may get stuck in pending status or may not be broadcast to the blockchain.
30
+
31
+ This is an undesirable situation:
32
+
33
+ - We could increase the nonce for the second transaction, but if the first one drops, the second one will also fail, even if the fees of the second one are high enough.
34
+ - If the wallet assumes the transaction was dropped and the nonce is reused, the network will interpret this unrelated second transaction as attempting to replace the first one (transaction replacement underprice error).
35
+ - The user submitting a second transaction to "retry" could be dangerous. Both transactions might get mined, causing the user to execute the swap twice instead of once.
36
+
37
+ One escape valve could be to update the `min`, `max`, or `gasPriceMultiplier` via remote config. This should be a temporary patch that needs to be rolled back once the node's gas price stabilizes.
38
+
39
+ ### Base Gas Price
40
+
41
+ The base gas price (or base fee) is the minimum amount of gas required to be paid per unit of gas to include a transaction in a block.
42
+
43
+ It can be found in `feeData.baseFeePerGas`. The fee monitor keeps it up to date by monitoring the latest block's information. It could be tuned by remote config, but this is not recommended.
44
+
45
+ The `feeData.baseFeePerGas` is used when showing an "estimated fee" in the UI and to calculate the max gas price. `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
46
+
47
+ ### Gas Limit
48
+
49
+ The `gasLimit` refers to the maximum amount of gas a transaction or block is allowed to consume during its execution.
50
+ Most EVMs assume 21,000 when transferring the main asset. Some EVMs, like Mantle, Arbitrum One, and Arbitrum Nova, use a different gas limit. These gas limits need to be estimated, similar to when performing smart contract calls.
51
+
52
+ For smart contract calls, like sending or swapping, the wallet estimates a gas limit based on the contract, the sender, the receiver, the amounts, etc. The gas limit is then bumped by a 20% `extraPercentage` as a safety net.
53
+
54
+ If the gas limit estimation fails, the code may fall back to a high default value defined per asset (`asset.gasLimit`).
55
+
56
+ Currently, there is no way to tune the gas limit resolution via remote config, and "extra percentage" values are hardcoded in the code.
57
+
58
+ Both the bumped `gasLimit` and the bumped `gasPrice` resolved when showing the fees are reused when sending.
59
+
60
+ The higher the gas limit and the gas price, the higher the max network fee, even though not all that gas may be used. The higher the max network fee, the lower the user's available balance.
61
+
62
+ ### Additional L2 Fees
63
+
64
+ L2 networks may have additional fees. Arbitrum One, Arbitrum Nova, and Mantle include the fees in the gas estimation (`forceGasLimitEstimation`). Base and Aptos need an additional fee estimation to get this extra fee (`estimateL1DataFee`).
65
+
66
+ The L2 extra fee needs to be considered when showing the fee to the user, resolving the available balance, and creating the transaction. The fee may be implicit in the transaction.
67
+
68
+ ### Custom Fees
69
+
70
+ Custom fees allow users to set a custom `gasPrice`. The wallet allows a range, as follows:
71
+
72
+ Note: `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
73
+
74
+ - `recommended: gasPrice`
75
+ - `min: gasPrice * gasPriceMinimumRate`
76
+ - `max: gasPrice * gasPriceMaximumRate`
77
+
78
+ If the user picks the recommended value, the custom fee is not set, leaving the default fee behavior.
79
+ Custom fees can be disabled by setting `rbfEnabled: false` via remote config. Currently, only Ethereum allows custom fees. Other EVMs could be included in the future.
80
+
81
+ The wallet allows acceleration when custom fees are allowed (and vice versa).
82
+
83
+ If the user picks a different custom fee in that range, the fee is calculated as `customFee * gasLimit`.
84
+
85
+ A transaction created with a low custom fee may be stuck for a while, blocking future transactions, and it may create issues with the nonce, as explained above.
86
+
87
+ Min and max values can be tuned by changing `gasPriceMinimumRate` and `gasPriceMaximumRate` via remote config.
88
+
89
+ ### TX formulas
90
+
91
+ Note: `gasPrice = baseFeePerGas * gasPriceMultiplier + tipGasPrice`
92
+
93
+ Table shows how the txs are create for both legacy and 1559 logic for major use case:
94
+
95
+ | | Send some main | Send all main | Send token/contract call | Send main with custom fee | Send token/contract with custom fee | Accelerate |
96
+ | ------------------ | -------------- | ------------- | ------------------------ | ------------------------- | ----------------------------------- | ------------------------------------------------------------ |
97
+ | Legacy Gas Price | gasPrice | gasPrice | gasPrice | customFee | customFee | max(current gasPrice,original tx gasPrice) \* BUMP_RATE 1.2 |
98
+ | Legacy Gas Limit | 21000 | 21000 | estimated + 20% extra | 21000 | estimated + 20% extra | original tx gasLimit |
99
+ | - | - | - | - | - | - | - |
100
+ | 1559 Max gas Price | gasPrice | gasPrice | gasPrice | customFee | customFee | max(current gasPrice, original tx gasPrice) \* BUMP_RATE 1.2 |
101
+ | 1559 Tip gas Price | tipGasPrice | gasPrice | tipGasPrice | customFee | tipGasPrice | orginal tx tipGasPrice \* BUMP_RATE 1.2 |
102
+ | 1559 Gas Limit | 21000 | 21000 | estimated + 20% extra | 21000 | estimated + 20% extra | original tx gasLimit |
103
+
104
+ ### Fee Data Example
105
+
106
+ This is the default fee data configuration for Ethereum. Other EVMs have different default fee data.
107
+
108
+ All these values can be tuned via remote config, although it may not be recommended as they are also tuned by the fee monitors:
109
+
110
+ - **`baseFeePerGas`:** `50 Gwei` – Reference value for the base gas price. It is quickly updated by the fee monitor.
111
+ - **`gasPrice`:** `77 Gwei` – `baseFeePerGas * gasPriceMultiplier + tipGasPrice`
112
+ - **`severGasPrice`:** `75 Gwei` – Reference value for the network gas price. It is quickly updated by the fee monitor.
113
+ - **`tipGasPrice`:** `2 Gwei` – Controls the `maxPriorityFeePerGas` for all transactions when EIP-1559 is enabled.
114
+ - **`eip1559Enabled`:** `true` – Enables or disables EIP-1559. A value of `false` means legacy fees and transactions.
115
+ - **`rbfEnabled`:** `true` – Enables custom fees and acceleration. Currently, this is only available for Ethereum but can be expanded to other EVMs.
116
+ - **`gasPriceMaximumRate`:** `1.3` – Controls the maximum value in the custom fee slider.
117
+ - **`gasPriceMinimumRate`:** `0.5` – Controls the minimum value in the custom fee slider.
118
+ - **`gasPriceMultiplier`:** `1` – Controls the `gasPrice` and `maxFeePerGas` of transactions, resolved as `current gas price * this multiplier`.
119
+ - **`max`:** `250 Gwei` – Controls the maximum gas price value that can be set.
120
+ - **`min`:** `1 Gwei` – Controls the minimum gas price value that can be set.
121
+ - **`fuelThreshold`:** `0.025 ETH` – If the balance falls below this value and the user holds tokens, a low balance warning is displayed.
122
+ - **`enableFeeDelegation`:** `false` – Not implemented.
123
+ - **`origin`:** `75 Gwei` – Legacy; consider removing.
124
+ - **`swapFee`:** `0.05 ETH` – Legacy; consider removing.
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "9.0.0-alpha.2",
4
- "description": "Ethereum Api",
3
+ "version": "9.0.0",
4
+ "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
5
+ "type": "module",
5
6
  "main": "src/index.js",
6
7
  "files": [
7
8
  "src",
@@ -9,55 +10,54 @@
9
10
  "!src/**/__tests__"
10
11
  ],
11
12
  "author": "Exodus Movement, Inc.",
12
- "license": "UNLICENSED",
13
+ "license": "MIT",
13
14
  "homepage": "https://github.com/ExodusMovement/assets/tree/master/ethereum/ethereum-api",
14
15
  "publishConfig": {
15
- "access": "restricted"
16
+ "access": "public",
17
+ "provenance": false
16
18
  },
17
19
  "scripts": {
18
- "test": "run -T jest",
19
- "lint": "run -T eslint .",
20
- "lint:fix": "yarn lint --fix"
20
+ "test": "exodus-test --jest"
21
21
  },
22
22
  "dependencies": {
23
- "@exodus/asset-lib": "^4.2.0",
24
- "@exodus/assets": "^9.1.1",
25
- "@exodus/basic-utils": "^2.1.0",
23
+ "@exodus/asset": "^2.0.4",
24
+ "@exodus/asset-lib": "^5.9.2",
25
+ "@exodus/assets": "^11.4.0",
26
+ "@exodus/basic-utils": "^3.0.1",
26
27
  "@exodus/bip44-constants": "^195.0.0",
27
- "@exodus/crypto": "^1.0.0-rc.0",
28
- "@exodus/currency": "^2.1.3",
29
- "@exodus/ethereum-lib": "^5.0.0",
30
- "@exodus/ethereum-meta": "^1.5.1",
31
- "@exodus/ethereumholesky-meta": "^1.0.3",
32
- "@exodus/ethereumjs-util": "^7.1.0-exodus.7",
28
+ "@exodus/crypto": "^1.0.0-rc.26",
29
+ "@exodus/currency": "^6.0.1",
30
+ "@exodus/ethereum-lib": "^5.25.0",
31
+ "@exodus/ethereum-meta": "^2.9.1",
32
+ "@exodus/ethereumjs": "^1.11.0",
33
33
  "@exodus/fetch": "^1.3.0",
34
- "@exodus/key-utils": "^3.1.0",
35
- "@exodus/models": "^11.0.0",
34
+ "@exodus/models": "^13.0.0",
35
+ "@exodus/safe-string": "^1.4.0",
36
+ "@exodus/send-validation-model": "^1.2.0",
36
37
  "@exodus/simple-retry": "^0.0.6",
37
- "@exodus/solidity-contract": "^1.1.3",
38
- "@exodus/web3-ethereum-utils": "^3.27.1",
38
+ "@exodus/solidity-contract": "^1.3.0",
39
+ "@exodus/traceparent": "^3.0.1",
40
+ "@exodus/web3-utils": "^1.51.2",
39
41
  "bn.js": "^5.2.1",
42
+ "delay": "^4.0.1",
43
+ "eventemitter3": "^4.0.7",
40
44
  "events": "^1.1.1",
41
45
  "idna-uts46-hx": "^2.3.1",
42
- "js-sha3": "^0.8.0",
43
46
  "lodash": "^4.17.15",
44
47
  "make-concurrent": "^4.0.0",
45
48
  "minimalistic-assert": "^1.0.1",
46
49
  "ms": "^2.1.1",
47
- "socket.io-client": "^2.1.1",
48
- "ws": "^6.1.0"
50
+ "socket.io-client": "^2.1.1"
49
51
  },
50
52
  "devDependencies": {
51
- "@exodus/assets": "^9.1.1",
52
53
  "@exodus/assets-testing": "^1.0.0",
53
- "@exodus/bsc-meta": "^1.2.2",
54
- "@exodus/ethereumarbone-meta": "^1.2.0",
55
- "@exodus/ethereumgoerli-meta": "^1.0.5",
56
- "@exodus/ethereumsepolia-meta": "^1.0.3",
57
- "@exodus/fantommainnet-meta": "^1.1.2",
58
- "@exodus/rootstock-meta": "^1.0.7",
59
- "cross-fetch": "^3.1.5",
60
- "delay": "4.0.1"
54
+ "@exodus/bsc-meta": "^2.5.1",
55
+ "@exodus/errors": "^3.8.0",
56
+ "@exodus/ethereumarbone-meta": "^2.1.2",
57
+ "@exodus/fantommainnet-meta": "^2.0.5",
58
+ "@exodus/matic-meta": "^2.2.7",
59
+ "@exodus/rootstock-meta": "^2.0.5",
60
+ "@exodus/send-validation": "^5.4.1"
61
61
  },
62
62
  "bugs": {
63
63
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Aethereum-api"
@@ -65,5 +65,6 @@
65
65
  "repository": {
66
66
  "type": "git",
67
67
  "url": "git+https://github.com/ExodusMovement/assets.git"
68
- }
68
+ },
69
+ "gitHead": "92d6cdde313e5244fb05a99b1be964a96f989c3b"
69
70
  }
@@ -1,6 +1,10 @@
1
1
  export const addressHasHistoryFactory =
2
2
  ({ server }) =>
3
3
  async (address) => {
4
- const history = await server.getHistoryV2(address, { index: 0, limit: 1 })
5
- return history.length > 0
4
+ const [nonceHex, balanceHex] = await Promise.all([
5
+ server.getTransactionCount(address, 'pending'),
6
+ server.getBalanceProxied(address, 'latest'),
7
+ ])
8
+
9
+ return Number.parseInt(nonceHex, 16) > 0 || Number.parseInt(balanceHex, 16) > 0
6
10
  }
@@ -2,12 +2,11 @@
2
2
  // before setting a non-zero allowance
3
3
  export const ZERO_ALLOWANCE_ASSETS = [
4
4
  'tetherusd',
5
- 'aragon',
6
- 'bancor',
7
- 'status',
5
+ 'bnt_ethereum_20e5a6cf',
6
+ 'snt_ethereum_181cbd8b',
8
7
  'decentraland',
9
- 'dent',
10
- 'lidodao',
8
+ 'dent_ethereum_6cf2defc',
9
+ 'ldo_ethereum_6d05833f',
11
10
  'enjincoin',
12
11
  ]
13
12
 
@@ -1,6 +1,11 @@
1
- import { fetchGasLimit } from '../gas-estimation'
2
- import { getServer } from '../exodus-eth-server/server-map'
3
- import { ALLOWANCE_TX_TIMEOUT, APPROVAL_GAS_LIMIT, ZERO_ALLOWANCE_ASSETS } from './constants'
1
+ import { isNumberUnit } from '@exodus/currency'
2
+ import { buffer2currency, createUnsignedTxFactory } from '@exodus/ethereum-lib'
3
+ import { bufferToInt } from '@exodus/ethereumjs/util'
4
+ import assert from 'minimalistic-assert'
5
+
6
+ import { getServer } from '../exodus-eth-server/index.js'
7
+ import { fetchGasLimit } from '../gas-estimation.js'
8
+ import { ALLOWANCE_TX_TIMEOUT, APPROVAL_GAS_LIMIT, ZERO_ALLOWANCE_ASSETS } from './constants.js'
4
9
 
5
10
  export const isZeroAllowanceAsset = (assetName) => ZERO_ALLOWANCE_ASSETS.includes(assetName)
6
11
 
@@ -27,62 +32,177 @@ export async function isSpendingApprovalRequired({
27
32
  return tokenAmount.gt(allowance)
28
33
  }
29
34
 
35
+ export const buildApproveTx = async ({
36
+ spenderAddress,
37
+ asset,
38
+ feeData,
39
+ fromAddress,
40
+ approveAmount = asset.currency.ZERO,
41
+ gasLimit,
42
+ txInput,
43
+ nonce,
44
+ walletAccount,
45
+ }) => {
46
+ assert(asset, 'expected asset')
47
+ assert(feeData, 'expected feeData')
48
+ assert(isNumberUnit(approveAmount), 'expected approveAmount')
49
+ assert(typeof spenderAddress === 'string', 'expected spenderAddress')
50
+ assert(walletAccount, 'expected walletAccount')
51
+
52
+ const { baseAsset } = asset
53
+ const chainId = baseAsset.chainId
54
+ assert(typeof chainId === 'number', 'expected chainId')
55
+
56
+ const createUnsignedTx = createUnsignedTxFactory({ chainId })
57
+
58
+ const toAddress = asset.contract.address
59
+ assert(typeof toAddress === 'string', 'expected contract address')
60
+
61
+ if (!txInput) txInput = asset.contract.approve.build(spenderAddress, approveAmount.toBaseString())
62
+
63
+ if (typeof nonce !== 'number') {
64
+ nonce = await baseAsset.getNonce({ asset, fromAddress, walletAccount })
65
+ }
66
+
67
+ if (!gasLimit) {
68
+ gasLimit = await fetchGasLimit({
69
+ asset: baseAsset,
70
+ amount: baseAsset.currency.ZERO,
71
+ toAddress,
72
+ txInput,
73
+ feeData,
74
+ isContract: true,
75
+ fromAddress,
76
+ })
77
+ }
78
+
79
+ return createUnsignedTx({
80
+ asset,
81
+ address: toAddress,
82
+ amount: baseAsset.currency.ZERO,
83
+ nonce,
84
+ txInput,
85
+ gasLimit,
86
+ gasPrice: feeData.gasPrice,
87
+ tipGasPrice: feeData.tipGasPrice,
88
+ fromAddress,
89
+ eip1559Enabled: feeData.eip1559Enabled,
90
+ })
91
+ }
92
+
93
+ // @temporary
94
+ // @deprecated use buildApproveTx instead
95
+ export const buildLegacyApproveTx = async ({
96
+ spenderAddress,
97
+ asset,
98
+ feeData,
99
+ fromAddress,
100
+ approveAmount,
101
+ gasLimit,
102
+ txInput,
103
+ nonce,
104
+ walletAccount,
105
+ }) => {
106
+ const unsignedTx = await buildApproveTx({
107
+ spenderAddress,
108
+ asset,
109
+ feeData,
110
+ fromAddress,
111
+ approveAmount,
112
+ gasLimit,
113
+ txInput,
114
+ nonce,
115
+ walletAccount,
116
+ })
117
+
118
+ const baseAsset = asset.baseAsset
119
+
120
+ return {
121
+ asset: baseAsset,
122
+ address: unsignedTx.txData.to,
123
+ amount: buffer2currency({ asset: baseAsset, value: unsignedTx.txData.value }),
124
+ txInput: unsignedTx.txData.data,
125
+ gasPrice: buffer2currency({ asset: baseAsset, value: unsignedTx.txData.gasPrice }),
126
+ tipGasPrice: unsignedTx.txData.tipGasPrice
127
+ ? buffer2currency({ asset: baseAsset, value: unsignedTx.txData.tipGasPrice })
128
+ : undefined,
129
+ gasLimit: bufferToInt(unsignedTx.txData.gasLimit),
130
+ fromAddress: unsignedTx.txMeta.fromAddress,
131
+ }
132
+ }
133
+
134
+ // @deprecated use buildApproveTx instead
30
135
  export const createApprove =
31
- ({ sendTx }) =>
136
+ ({ assetClientInterface } = Object.create(null)) =>
32
137
  async ({
33
138
  spenderAddress,
34
139
  asset,
35
140
  feeData,
141
+ fromAddress,
36
142
  approveAmount,
37
143
  gasLimit,
38
- txInput: preTxInput,
39
- ...rest
144
+ txInput,
145
+ nonce,
146
+ walletAccount,
147
+ ...extras
40
148
  }) => {
41
- const baseAsset = asset.baseAsset
42
- const txInput =
43
- preTxInput ?? asset.contract.approve.build(spenderAddress, approveAmount.toBaseString())
149
+ if (!assetClientInterface) {
150
+ const approveTx = await buildLegacyApproveTx({
151
+ spenderAddress,
152
+ asset,
153
+ feeData,
154
+ fromAddress,
155
+ approveAmount,
156
+ gasLimit,
157
+ txInput,
158
+ nonce,
159
+ walletAccount,
160
+ })
44
161
 
45
- const toAddress = asset.contract.address
46
- const gasLimitOptions = {
47
- asset: baseAsset,
48
- amount: baseAsset.currency.ZERO,
49
- toAddress,
50
- txInput,
51
- feeData,
52
- isContract: true,
53
- }
162
+ const { unsignedTx } = await asset.baseAsset.api.createTx({
163
+ walletAccount,
164
+ ...approveTx,
165
+ ...extras,
166
+ })
167
+ const txData = await asset.baseAsset.api.sendTx({
168
+ asset: asset.baseAsset,
169
+ walletAccount,
170
+ unsignedTx,
171
+ })
54
172
 
55
- gasLimit = gasLimit || (await fetchGasLimit(gasLimitOptions))
173
+ if (!txData || !txData.txId) {
174
+ throw new Error(`Failed to approve ${asset.displayTicker} - ${spenderAddress}`)
175
+ }
56
176
 
57
- const txData = await sendTx({
58
- asset: baseAsset.name,
59
- receiver: {
60
- address: toAddress,
61
- amount: baseAsset.currency.ZERO,
62
- },
63
- txInput,
64
- gasPrice: feeData.gasPrice,
177
+ return txData
178
+ }
179
+
180
+ const unsignedTx = await buildApproveTx({
181
+ spenderAddress,
182
+ asset,
183
+ feeData,
184
+ fromAddress,
185
+ approveAmount,
65
186
  gasLimit,
66
- silent: true,
67
- ...rest,
187
+ txInput,
188
+ nonce,
189
+ walletAccount,
68
190
  })
69
191
 
70
- if (!txData || !txData.txId) {
71
- throw new Error(`Failed to approve ${asset.displayTicker} - ${spenderAddress}`)
72
- }
192
+ const { txId } = await asset.baseAsset.api.sendTx({ asset, walletAccount, unsignedTx })
73
193
 
74
- return txData
194
+ return { txId }
75
195
  }
76
196
 
77
197
  const createSendApprovalAndWatchConfirmation =
78
- ({ sendTx, watchTxConfirmation }) =>
198
+ ({ watchTxConfirmation }) =>
79
199
  async (data) => {
80
200
  // To change the approve amount you first have to reduce the addresses`
81
201
  // allowance to zero by calling `approve(_spender, 0)` if it is not
82
202
  // already 0 to mitigate the race condition described here:
83
203
  // see: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
84
204
  // see: https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code
85
- const approve = createApprove({ sendTx })
205
+ const approve = createApprove()
86
206
  const { txId } = await approve(data)
87
207
  await watchTxConfirmation(
88
208
  { asset: data.asset.baseAsset.name, walletAccount: data.walletAccount },
@@ -94,11 +214,10 @@ const createSendApprovalAndWatchConfirmation =
94
214
  }
95
215
 
96
216
  export const createApproveSpendingTokens =
97
- ({ sendTx, watchTxConfirmation }) =>
217
+ ({ watchTxConfirmation }) =>
98
218
  async (data) => {
99
219
  const txIds = []
100
220
  const sendApprovalAndWatchConfirmation = createSendApprovalAndWatchConfirmation({
101
- sendTx,
102
221
  watchTxConfirmation,
103
222
  })
104
223
  if (ZERO_ALLOWANCE_ASSETS.includes(data.asset.name) && !data.approveAmount.isZero) {
@@ -110,14 +229,14 @@ export const createApproveSpendingTokens =
110
229
 
111
230
  txIds.push(revokeTokenApprovalTxId)
112
231
  // Hardcoded min gas limit. Otherwise `fetchGasLimit` will fail since `approve(_spender, 0)` is not yet confirmed
113
- // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
232
+ // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
114
233
  data.gasLimit = Math.max(data.gasLimit || 0, APPROVAL_GAS_LIMIT)
115
234
  }
116
235
 
117
- const tokenApprovalTxId = await sendApprovalAndWatchConfirmation({ ...data, sendTx })
236
+ const tokenApprovalTxId = await sendApprovalAndWatchConfirmation(data)
118
237
  txIds.push(tokenApprovalTxId)
119
238
 
120
239
  return txIds
121
240
  }
122
241
 
123
- export { APPROVAL_GAS_LIMIT, ZERO_ALLOWANCE_ASSETS } from './constants'
242
+ export { APPROVAL_GAS_LIMIT, ZERO_ALLOWANCE_ASSETS } from './constants.js'
@@ -0,0 +1,23 @@
1
+ import { fetchival } from '@exodus/fetch'
2
+ import assert from 'minimalistic-assert'
3
+
4
+ export const DEFAULT_ASSESS_TRANSACTION_API_URL =
5
+ 'https://simulation.a.exodus.io/simulation/transaction/assessment'
6
+
7
+ export const createAssessTransaction = (
8
+ { apiUrl = DEFAULT_ASSESS_TRANSACTION_API_URL, headers, request = fetchival } = Object.create(
9
+ null
10
+ )
11
+ ) => {
12
+ assert(typeof apiUrl === 'string' && apiUrl.length > 0, 'apiUrl must be a non-empty string')
13
+
14
+ return async function assessTransaction({ payload }) {
15
+ return request(apiUrl, {
16
+ method: 'POST',
17
+ headers: {
18
+ ...headers,
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ }).post(payload)
22
+ }
23
+ }
@@ -0,0 +1,104 @@
1
+ import { memoizeLruCache } from '@exodus/asset-lib'
2
+ import assert from 'minimalistic-assert'
3
+ import ms from 'ms'
4
+
5
+ const DEFAULT_TIMEOUT_MS = 5000
6
+ const DEFAULT_CACHE_MAX = 500
7
+ // Short TTL to dedupe rapid retypes without holding stale verdicts.
8
+ const DEFAULT_CACHE_TTL_MS = ms('1m')
9
+ const GENERIC_RISK_REASON = 'This transaction was flagged as risky.'
10
+ const RECIPIENT_INVOLVEMENT_TYPES = new Set(['scammer', 'destination'])
11
+
12
+ const normalizeAddress = (address) => (typeof address === 'string' ? address.toLowerCase() : '')
13
+
14
+ // `input` and `hash` are part of the key so different calldata gets its own verdict.
15
+ const buildCacheKey = ({ chain, fromAddress, toAddress, value, input, hash }) =>
16
+ [
17
+ chain,
18
+ normalizeAddress(fromAddress),
19
+ normalizeAddress(toAddress),
20
+ String(value),
21
+ input ?? '',
22
+ hash ?? '',
23
+ ].join(':')
24
+
25
+ const isRecipientFinding = ({ finding, recipientAddress }) => {
26
+ const relatedAssets = Array.isArray(finding?.relatedAssets) ? finding.relatedAssets : []
27
+ return relatedAssets.some((relatedAsset) => {
28
+ if (normalizeAddress(relatedAsset?.address) !== normalizeAddress(recipientAddress)) {
29
+ return false
30
+ }
31
+
32
+ const involvementTypes = Array.isArray(relatedAsset.involvementTypes)
33
+ ? relatedAsset.involvementTypes
34
+ : []
35
+ return involvementTypes.some((type) => RECIPIENT_INVOLVEMENT_TYPES.has(type))
36
+ })
37
+ }
38
+
39
+ const findRecipientFinding = ({ findings, recipientAddress }) => {
40
+ if (!Array.isArray(findings) || !recipientAddress) return
41
+ return findings.find((candidate) => isRecipientFinding({ finding: candidate, recipientAddress }))
42
+ }
43
+
44
+ const parseAssessment = ({ body, recipientAddress }) => {
45
+ if (!body?.success) return { action: 'NONE' }
46
+ if (body.data?.recommendation !== 'deny') return { action: 'NONE' }
47
+ const finding = findRecipientFinding({ findings: body.data?.findings, recipientAddress })
48
+ if (!finding) return { action: 'NONE' }
49
+
50
+ return {
51
+ action: 'WARN',
52
+ reason: finding.title || GENERIC_RISK_REASON,
53
+ }
54
+ }
55
+
56
+ // Rejects after `timeoutMs`. The underlying request keeps running because the
57
+ // API helper doesn't accept an AbortSignal.
58
+ const withTimeout = async (promise, timeoutMs) => {
59
+ let timeoutId
60
+ const timeoutPromise = new Promise((_resolve, reject) => {
61
+ timeoutId = setTimeout(() => reject(new Error('checkTx: timeout')), timeoutMs)
62
+ })
63
+ try {
64
+ return await Promise.race([promise, timeoutPromise])
65
+ } finally {
66
+ clearTimeout(timeoutId)
67
+ }
68
+ }
69
+
70
+ const noopLogger = { warn: () => {} }
71
+
72
+ export const createCheckTx = (
73
+ { timeout = DEFAULT_TIMEOUT_MS, makeApiCall, logger = noopLogger } = Object.create(null)
74
+ ) => {
75
+ assert(typeof makeApiCall === 'function', 'makeApiCall is required')
76
+
77
+ const warn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : noopLogger.warn
78
+
79
+ // Errors and timeouts throw and are evicted by memoize-lru-cache, so only
80
+ // vendor-confirmed verdicts get cached.
81
+ const fetchVerdict = memoizeLruCache(
82
+ async ({ chain, fromAddress, toAddress, value, input, hash }) => {
83
+ const transaction = { chain, fromAddress, toAddress, value }
84
+ if (input !== undefined) transaction.input = input
85
+ if (hash !== undefined) transaction.hash = hash
86
+
87
+ const body = await withTimeout(makeApiCall({ payload: { transaction } }), timeout)
88
+
89
+ if (!body) throw new Error('checkTx: empty response')
90
+ return parseAssessment({ body, recipientAddress: toAddress })
91
+ },
92
+ buildCacheKey,
93
+ { max: DEFAULT_CACHE_MAX, maxAge: DEFAULT_CACHE_TTL_MS }
94
+ )
95
+
96
+ return async (payload) => {
97
+ try {
98
+ return await fetchVerdict(payload)
99
+ } catch (error) {
100
+ warn('checkTx: failing open', error)
101
+ return { action: 'NONE' }
102
+ }
103
+ }
104
+ }
@@ -0,0 +1 @@
1
+ export { createCheckTx } from './create-check-tx.js'