@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
package/src/gas-estimation.js
CHANGED
|
@@ -1,113 +1,230 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { currency2buffer, isToken } from '@exodus/ethereum-lib'
|
|
4
|
-
import { estimateGas, isContractAddressCached } from './eth-like-util'
|
|
1
|
+
import { currency2buffer, isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
2
|
+
import { bufferToHex, toBuffer } from '@exodus/ethereumjs/util'
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
import { estimateGas, isContractAddressCached, isForwarderContractCached } from './eth-like-util.js'
|
|
5
|
+
import {
|
|
6
|
+
ARBITRARY_ADDRESS,
|
|
7
|
+
resolveCriticalTxAttributes,
|
|
8
|
+
TX_TYPE_TRANSFER,
|
|
9
|
+
} from './tx-type/index.js'
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_GAS_LIMIT_MULTIPLIER = 1.29
|
|
7
12
|
|
|
8
13
|
// 16 gas per non-zero byte (4 gas per zero byte) of "transaction input data"
|
|
9
14
|
const GAS_PER_NON_ZERO_BYTE = 16
|
|
10
15
|
|
|
11
16
|
export const DEFAULT_CONTRACT_GAS_LIMIT = 1e6
|
|
17
|
+
export const DEFAULT_TOKEN_GAS_LIMIT = 120e3
|
|
18
|
+
export const DEFAULT_GAS_LIMIT = 21_000
|
|
19
|
+
|
|
20
|
+
// HACK: RPCs generally provide imprecise estimates
|
|
21
|
+
// for `gasUsed` (often these are insufficient).
|
|
22
|
+
export const scaleGasLimitEstimate = ({
|
|
23
|
+
estimatedGasLimit,
|
|
24
|
+
gasLimitMultiplier = DEFAULT_GAS_LIMIT_MULTIPLIER,
|
|
25
|
+
}) => {
|
|
26
|
+
const gasLimit = parseInt(estimatedGasLimit)
|
|
27
|
+
if (!Number.isInteger(gasLimit)) {
|
|
28
|
+
throw new TypeError('Invalid scaleGasLimitEstimate ' + estimatedGasLimit)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const multiplier = parseFloat(gasLimitMultiplier)
|
|
32
|
+
if (Number.isNaN(multiplier)) {
|
|
33
|
+
throw new TypeError('Invalid gasLimitMultiplier ' + gasLimitMultiplier)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Math.floor(gasLimit * multiplier)
|
|
37
|
+
}
|
|
12
38
|
|
|
13
39
|
// Starting with geth v1.9.14, if gasPrice is set for eth_estimateGas call, the call allowance will
|
|
14
40
|
// be calculated with account's balance divided by gasPrice. If user's balance is too low,
|
|
15
41
|
// the gasEstimation will fail. If gasPrice is set to '0x0', the account's balance is not
|
|
16
42
|
// used to estimate gas.
|
|
17
|
-
export async function estimateGasLimit(
|
|
43
|
+
export async function estimateGasLimit({
|
|
18
44
|
asset,
|
|
19
45
|
fromAddress,
|
|
20
46
|
toAddress,
|
|
21
|
-
|
|
47
|
+
// TODO: rename to value, as this is `msg.value`.
|
|
48
|
+
amount = asset.currency.ZERO,
|
|
22
49
|
data,
|
|
23
50
|
gasPrice = '0x',
|
|
24
|
-
|
|
25
|
-
) {
|
|
51
|
+
}) {
|
|
26
52
|
const opts = {
|
|
27
53
|
from: fromAddress,
|
|
28
54
|
to: toAddress,
|
|
29
55
|
value: normalizeAmount(amount),
|
|
30
|
-
data: Buffer.isBuffer(data) ?
|
|
56
|
+
data: Buffer.isBuffer(data) ? bufferToHex(data) : data,
|
|
31
57
|
gasPrice: normalizeGasPrice(gasPrice),
|
|
32
58
|
}
|
|
33
59
|
|
|
34
60
|
const estimatedGas = await estimateGas({ asset, ...opts })
|
|
35
|
-
return
|
|
36
|
-
.imuln(100 + extraPercentage)
|
|
37
|
-
.idivn(100)
|
|
38
|
-
.toNumber()
|
|
61
|
+
return Number(BigInt(estimatedGas))
|
|
39
62
|
}
|
|
40
63
|
|
|
41
|
-
export async function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
toAddress,
|
|
45
|
-
txInput = '0x',
|
|
46
|
-
amount,
|
|
47
|
-
bip70,
|
|
48
|
-
throwOnError = true,
|
|
49
|
-
extraPercentage,
|
|
50
|
-
}) {
|
|
51
|
-
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice)
|
|
52
|
-
return asset.name === 'ethereum' ? 65_000 : 130_000 // from on chain stats https://dune.xyz/queries/189123
|
|
64
|
+
export async function resolveGasLimitMultiplier({ asset, feeData, toAddress, fromAddress }) {
|
|
65
|
+
const gasLimitMultiplierWhenUnknownAddress =
|
|
66
|
+
feeData?.gasLimits?.[asset.name]?.gasLimitMultiplierWhenUnknownAddress
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
// NOTE: If either the `fromAddress` or `toAddress` has fell
|
|
69
|
+
// back to the `ARBITRARY_ADDRESS`, these qualify as
|
|
70
|
+
// "unknown" addresses.
|
|
71
|
+
const fromAddressIsUnknown = fromAddress === ARBITRARY_ADDRESS
|
|
72
|
+
const toAddressIsUnknown = toAddress === ARBITRARY_ADDRESS
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
if (gasLimitMultiplierWhenUnknownAddress && (fromAddressIsUnknown || toAddressIsUnknown)) {
|
|
75
|
+
return gasLimitMultiplierWhenUnknownAddress
|
|
76
|
+
}
|
|
58
77
|
|
|
59
|
-
const
|
|
78
|
+
const gasLimitMultiplier = feeData?.gasLimits?.[asset.name]?.gasLimitMultiplier
|
|
60
79
|
|
|
61
|
-
|
|
80
|
+
if (gasLimitMultiplier) {
|
|
81
|
+
return gasLimitMultiplier
|
|
82
|
+
}
|
|
62
83
|
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
if (asset.baseAsset.estimateL1DataFee) {
|
|
85
|
+
return 2
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isToken = asset.name !== asset.baseAsset.name
|
|
89
|
+
|
|
90
|
+
// calling forwarder contracts with a bumped gas limit causes 'Out Of Gas' error on chain
|
|
91
|
+
const hasForwarderContract =
|
|
92
|
+
!isToken && toAddress ? await isForwarderContractCached({ asset, address: toAddress }) : false
|
|
93
|
+
|
|
94
|
+
if (hasForwarderContract) {
|
|
95
|
+
return 1
|
|
96
|
+
}
|
|
70
97
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
return DEFAULT_GAS_LIMIT_MULTIPLIER
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// https://eips.ethereum.org/EIPS/eip-7623
|
|
102
|
+
export const getEip7623CalldataCost = (calldata) => {
|
|
103
|
+
calldata = toBuffer(calldata)
|
|
104
|
+
|
|
105
|
+
const TOTAL_COST_FLOOR_PER_TOKEN = 10
|
|
106
|
+
|
|
107
|
+
const nonZeroBytesInCalldata = calldata.filter(Boolean).length
|
|
108
|
+
const zeroBytesInCalldata = calldata.length - nonZeroBytesInCalldata
|
|
109
|
+
const tokensInCalldata = zeroBytesInCalldata + nonZeroBytesInCalldata * 4
|
|
110
|
+
|
|
111
|
+
return TOTAL_COST_FLOOR_PER_TOKEN * tokensInCalldata
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const defaultGasLimit = ({ asset, txInput }) => {
|
|
115
|
+
const isToken = isEthereumLikeToken(asset)
|
|
116
|
+
|
|
117
|
+
const calldataCost = asset.baseAsset.eip7623Supported
|
|
118
|
+
? getEip7623CalldataCost(txInput)
|
|
119
|
+
: GAS_PER_NON_ZERO_BYTE * toBuffer(txInput).length
|
|
120
|
+
|
|
121
|
+
return (isToken ? DEFAULT_TOKEN_GAS_LIMIT : DEFAULT_GAS_LIMIT) + calldataCost
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// TODO: `gasLimit` needs to be a responsibility of `resolveTxAttributesByTxType`.
|
|
125
|
+
export async function fetchGasLimit({
|
|
126
|
+
asset,
|
|
127
|
+
feeData,
|
|
128
|
+
fromAddress: providedFromAddress,
|
|
129
|
+
toAddress: providedToAddress,
|
|
130
|
+
txInput: providedTxInput,
|
|
131
|
+
txValue: providedTxValue,
|
|
132
|
+
amount: providedAmount,
|
|
133
|
+
contractAddress: providedTxToAddress,
|
|
134
|
+
bip70,
|
|
135
|
+
txType = TX_TYPE_TRANSFER,
|
|
136
|
+
throwOnError = true,
|
|
137
|
+
isSendAll = false,
|
|
138
|
+
}) {
|
|
139
|
+
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice) {
|
|
140
|
+
// from on chain stats https://dune.xyz/queries/189123
|
|
141
|
+
return asset.name === 'ethereum' ? 65_000 : 130_000
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// TODO: If we can provide the `walletAccount` via `fetchGasLimit` then
|
|
145
|
+
// we can use `resolveTxAttributesByTxType` instead (and remove
|
|
146
|
+
// the concept of critical attributes).
|
|
147
|
+
const { txToAddress, txInput, txValue } = resolveCriticalTxAttributes({
|
|
148
|
+
asset,
|
|
149
|
+
amount: providedAmount,
|
|
150
|
+
toAddress: providedToAddress,
|
|
151
|
+
txToAddress: providedTxToAddress,
|
|
152
|
+
txInput: providedTxInput,
|
|
153
|
+
txValue: providedTxValue,
|
|
154
|
+
txType,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const isContractTxToAddress =
|
|
158
|
+
Boolean(txToAddress) && (await isContractAddressCached({ asset, address: txToAddress }))
|
|
159
|
+
|
|
160
|
+
const fromAddress = providedFromAddress ?? ARBITRARY_ADDRESS
|
|
161
|
+
|
|
162
|
+
const isToken = isEthereumLikeToken(asset)
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
txType === TX_TYPE_TRANSFER &&
|
|
166
|
+
!isToken &&
|
|
167
|
+
!isContractTxToAddress &&
|
|
168
|
+
!asset.forceGasLimitEstimation
|
|
83
169
|
) {
|
|
84
|
-
return defaultGasLimit()
|
|
170
|
+
return defaultGasLimit({ asset, txInput })
|
|
85
171
|
}
|
|
86
172
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
173
|
+
const gasLimitMultiplier = await resolveGasLimitMultiplier({
|
|
174
|
+
asset,
|
|
175
|
+
feeData,
|
|
176
|
+
fromAddress,
|
|
177
|
+
toAddress: txToAddress,
|
|
178
|
+
})
|
|
91
179
|
|
|
92
180
|
try {
|
|
93
|
-
return
|
|
181
|
+
// NOTE: Although we'll return the `fixedGasLimit` for known
|
|
182
|
+
// tokens, we'll still want to execute `estimateGasLimit`
|
|
183
|
+
// to verify their transaction actually succeeds (i.e.
|
|
184
|
+
// they aren't trying to transfer more than their balance).
|
|
185
|
+
//
|
|
186
|
+
// This prevents users from submitting `fixedGasLimit`
|
|
187
|
+
// token transactions which result in a `revert`.
|
|
188
|
+
const estimatedGasLimit = await estimateGasLimit({
|
|
94
189
|
asset,
|
|
95
190
|
fromAddress,
|
|
96
|
-
toAddress,
|
|
97
|
-
amount,
|
|
98
|
-
txInput,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
191
|
+
toAddress: txToAddress,
|
|
192
|
+
amount: txValue,
|
|
193
|
+
data: txInput,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const scaledGasLimitEstimate = scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
|
|
197
|
+
if (!isToken) {
|
|
198
|
+
// For native send-all on EOA targets, return the raw `eth_estimateGas`
|
|
199
|
+
// result (no 2x safety pad). Combined with the EIP-1559 tip override
|
|
200
|
+
// applied in tx-create.js (also gated on `isSendAll && !isContract`),
|
|
201
|
+
// this is the no-dust path: gasLimit ≈ gasUsed, so nothing comes back
|
|
202
|
+
// as refund. For contracts we keep the safety pad because contract
|
|
203
|
+
// gas can be nondeterministic against estimates.
|
|
204
|
+
return isSendAll && !isContractTxToAddress ? estimatedGasLimit : scaledGasLimitEstimate
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// NOTE: If we've enabled `fixGasLimit`s for a token,
|
|
208
|
+
// we need to make sure that transaction we're
|
|
209
|
+
// making actually targets that token.
|
|
210
|
+
//
|
|
211
|
+
// Otherwise, we risk applying the fixed gas limit
|
|
212
|
+
// to transactions with _other_ smart contracts
|
|
213
|
+
// simply because the caller specified the token
|
|
214
|
+
// asset.
|
|
215
|
+
const isTokenTransaction = asset.contract.address.toLowerCase() === txToAddress.toLowerCase()
|
|
216
|
+
if (!isTokenTransaction) return scaledGasLimitEstimate
|
|
217
|
+
|
|
218
|
+
return feeData?.gasLimits?.[asset.name]?.fixedGasLimit ?? scaledGasLimitEstimate
|
|
102
219
|
} catch (err) {
|
|
103
220
|
if (throwOnError) throw err
|
|
221
|
+
|
|
104
222
|
console.error('fetchGasLimit error', err)
|
|
105
223
|
|
|
106
224
|
// fallback value for contract case
|
|
107
|
-
if (
|
|
225
|
+
if (isContractTxToAddress) return DEFAULT_CONTRACT_GAS_LIMIT
|
|
108
226
|
|
|
109
|
-
|
|
110
|
-
return defaultGasLimit()
|
|
227
|
+
return defaultGasLimit({ asset, txInput })
|
|
111
228
|
}
|
|
112
229
|
}
|
|
113
230
|
|
|
@@ -116,7 +233,7 @@ function normalizeAmount(amount) {
|
|
|
116
233
|
amount = currency2buffer(amount)
|
|
117
234
|
}
|
|
118
235
|
|
|
119
|
-
amount =
|
|
236
|
+
amount = bufferToHex(amount)
|
|
120
237
|
while (amount[2] === '0') amount = '0x' + amount.slice(3)
|
|
121
238
|
if (amount === '0x') amount = '0x0'
|
|
122
239
|
|
|
@@ -4,7 +4,7 @@ export const createGetBalanceForAddress = ({ asset, server }) => {
|
|
|
4
4
|
assert(asset, 'asset is required')
|
|
5
5
|
assert(server, 'server is required')
|
|
6
6
|
return async (address) => {
|
|
7
|
-
const balance = await server.
|
|
7
|
+
const balance = await server.getBalanceProxied(address)
|
|
8
8
|
if (typeof balance === 'string') {
|
|
9
9
|
// hex-response from eth_getBalance node
|
|
10
10
|
return asset.currency.baseUnit(balance)
|
package/src/get-balances.js
CHANGED
|
@@ -1,29 +1,241 @@
|
|
|
1
|
-
import { isRpcBalanceAsset } from '@exodus/ethereum-lib'
|
|
2
|
-
|
|
3
|
-
import { get } from 'lodash'
|
|
4
|
-
import assert from 'minimalistic-assert'
|
|
5
|
-
|
|
6
1
|
import {
|
|
7
2
|
getBalanceFromAccountState,
|
|
3
|
+
getBalanceFromAccountStateIfAny,
|
|
8
4
|
getBalanceFromTxLog,
|
|
9
5
|
getUnconfirmedReceivedBalance,
|
|
10
6
|
getUnconfirmedSentBalance,
|
|
11
7
|
} from '@exodus/asset-lib'
|
|
8
|
+
import assert from 'minimalistic-assert'
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
import { getLatestCanonicalAbsoluteBalanceTx } from './tx-log/clarity-utils/index.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Native balance helper for {@link getAbsoluteBalance}. Only consults
|
|
14
|
+
* `accountState` when the txLog is empty; otherwise the caller walks the
|
|
15
|
+
* txLog for a `balanceChange`. Inside the empty-txLog branch, picks a
|
|
16
|
+
* signal in decreasing order of trust:
|
|
17
|
+
*
|
|
18
|
+
* 1. Non-zero `accountState.balance` (authoritative — RPC `eth_getBalance`
|
|
19
|
+
* result or `accountInfo` `account_balance` row from truncated history).
|
|
20
|
+
* 2. `truncatedAccountState.balance` (state at the cursor), default ZERO.
|
|
21
|
+
*
|
|
22
|
+
* ZERO is treated as "no signal" because `EthereumLikeAccountState` defaults
|
|
23
|
+
* `balance` to ZERO; we cannot distinguish "model default" from "chain says
|
|
24
|
+
* you hold 0". The truncated baseline acts as the explicit fallback.
|
|
25
|
+
*/
|
|
26
|
+
const getBaseBalanceFromAccountState = ({ asset, txLog, accountState }) => {
|
|
27
|
+
if (txLog.size === 0) {
|
|
28
|
+
const balance = getBalanceFromAccountStateIfAny({ asset, accountState })
|
|
29
|
+
if (balance && !balance.isZero) return balance
|
|
30
|
+
|
|
31
|
+
return getTruncatedBaseBalance({ accountState }) ?? asset.currency.ZERO
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Token counterpart of {@link getBaseBalance}. Tokens are sparse — there's no
|
|
39
|
+
* model default for `tokenBalances[name]`, so any value present (including an
|
|
40
|
+
* explicit ZERO from `accountInfo` after the user drained the token) is
|
|
41
|
+
* authoritative and returned as-is. Only when the key is missing entirely do
|
|
42
|
+
* we fall through to the truncated baseline / txLog walk.
|
|
43
|
+
*/
|
|
44
|
+
const getTokenBalanceFromAccountState = ({ asset, txLog, accountState }) => {
|
|
45
|
+
const balance = getBalanceFromAccountStateIfAny({ asset, accountState })
|
|
46
|
+
if (balance) return balance
|
|
47
|
+
|
|
48
|
+
if (txLog.size === 0) {
|
|
49
|
+
return getTruncatedTokenBalance({ asset, accountState }) ?? asset.currency.ZERO
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cursor-baseline native balance from {@link accountState.truncatedAccountState},
|
|
57
|
+
* sourced from Clarity `truncated` `accountInfo` rows on initial `cursor=0`
|
|
58
|
+
* fetch. Returns `undefined` when the snapshot isn't populated (e.g. the
|
|
59
|
+
* server didn't ship a baseline for this account).
|
|
60
|
+
*/
|
|
61
|
+
const getTruncatedBaseBalance = ({ accountState }) => accountState?.truncatedAccountState?.balance
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Token counterpart of {@link getTruncatedBaseBalance}.
|
|
65
|
+
*/
|
|
66
|
+
const getTruncatedTokenBalance = ({ asset, accountState }) =>
|
|
67
|
+
accountState?.truncatedAccountState?.tokenBalances?.[asset.name]
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* TODO: Balance model gaps for EVM staking assets
|
|
71
|
+
*
|
|
72
|
+
* Missing fields that should be added to getBalancesFactory return value:
|
|
73
|
+
*
|
|
74
|
+
* 1. `rewards` / `liquidRewards` - Currently, accountState.staking[asset.name] contains
|
|
75
|
+
* rewardsBalance and liquidRewards (from asset.api.staking.getStakingInfo → accountState.staking),
|
|
76
|
+
* but getBalancesFactory doesn't read or expose them. Need a getRewards() helper similar
|
|
77
|
+
* to getStaked() and getStaking().
|
|
78
|
+
*
|
|
79
|
+
* 2. `rewardsBalance` (total lifetime rewards) - Available in accountState but not surfaced.
|
|
80
|
+
*
|
|
81
|
+
* 3. `withdrawable` - For Polygon, this represents claimable rewards, distinct from
|
|
82
|
+
* unclaimedUndelegatedBalance (unstaked principal). Currently conflated.
|
|
83
|
+
*
|
|
84
|
+
* 4. `walletReserve` - Documented in balances-model.md but not implemented.
|
|
85
|
+
*
|
|
86
|
+
* 5. `staking` field reads pendingBalance + pendingDepositedBalance, which Polygon staking
|
|
87
|
+
* doesn't return (only Ethereum native staking uses these). Needs asset-specific handling.
|
|
88
|
+
*
|
|
89
|
+
* The data flow already exists: monitor hooks call getStakingInfo() → accountState.staking[asset.name].
|
|
90
|
+
* Implementation would add helper functions (getLiquidRewards, getRewardsBalance, etc.) and
|
|
91
|
+
* include these in the returned balance object, standardized across Ethereum/Polygon/etc.
|
|
92
|
+
*
|
|
93
|
+
* See: docs/balances-model.md for the intended balance model spec.
|
|
94
|
+
*/
|
|
95
|
+
export const getAbsoluteBalance = ({ asset, txLog, accountState }) => {
|
|
96
|
+
assert(asset, 'asset is required')
|
|
97
|
+
assert(txLog, 'txLog is required')
|
|
98
|
+
|
|
99
|
+
const isBase = asset.name === asset.baseAsset.name
|
|
100
|
+
const accountStateBalance = isBase
|
|
101
|
+
? getBaseBalanceFromAccountState({ asset, txLog, accountState })
|
|
102
|
+
: getTokenBalanceFromAccountState({ asset, txLog, accountState })
|
|
103
|
+
if (accountStateBalance) {
|
|
104
|
+
// Authoritative `accountState` snapshot (or empty-txLog terminal value).
|
|
105
|
+
return accountStateBalance
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// NOTE: We reverse the `txLog` to prioritize the handling
|
|
109
|
+
// of the most recent transactions first. The reason
|
|
110
|
+
// we do this is to terminate early if we find an
|
|
111
|
+
// absolute balance node.
|
|
112
|
+
const reversedTxLog = txLog.reverse()
|
|
113
|
+
|
|
114
|
+
const maybeLatestAbsoluteBalanceTx = getLatestCanonicalAbsoluteBalanceTx({ reversedTxLog })
|
|
115
|
+
|
|
116
|
+
let balance = asset.currency.ZERO
|
|
117
|
+
let hasAbsoluteBalance = false
|
|
118
|
+
|
|
119
|
+
for (const tx of reversedTxLog) {
|
|
120
|
+
if (tx.data.balanceChange) {
|
|
121
|
+
// If we are aware of the existence of the most
|
|
122
|
+
// recent absolute balance, then we can ignore
|
|
123
|
+
// any competing instances.
|
|
124
|
+
if (maybeLatestAbsoluteBalanceTx && tx.txId !== maybeLatestAbsoluteBalanceTx.txId) {
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hasAbsoluteBalance = true
|
|
129
|
+
balance = balance.add(asset.currency.baseUnit(tx.data.balanceChange.to))
|
|
130
|
+
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// NOTE: Avoids processing any unconfirmed balances, since
|
|
135
|
+
// `getAbsoluteBalance` only processes a subset of
|
|
136
|
+
// transactions up until the first `balanceChange` event
|
|
137
|
+
// is encountered, but `getBalancesFactory` takes all
|
|
138
|
+
// all pending amounts into consideration (i.e. those
|
|
139
|
+
// at a greater depth than the most recent `balanceChange`
|
|
140
|
+
// event).
|
|
141
|
+
if (!tx.failed && tx.pending) continue
|
|
142
|
+
|
|
143
|
+
if (!(tx.dropped || tx.data.replacedBy)) {
|
|
144
|
+
if (!tx.error) {
|
|
145
|
+
balance = balance.add(tx.coinAmount)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (tx.feeAmount?.unitType.equals(tx.coinAmount.unitType)) {
|
|
149
|
+
balance = balance.sub(tx.feeAmount)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!hasAbsoluteBalance) {
|
|
155
|
+
// Last line of defense: under truncated history the post-cursor txLog can
|
|
156
|
+
// omit the most recent `balanceChange` (e.g. only a pending send remains).
|
|
157
|
+
// Anchor on the `accountInfo` snapshot, mirroring the same precedence as
|
|
158
|
+
// {@link getBaseBalanceFromAccountState} / {@link getTokenBalanceFromAccountState}:
|
|
159
|
+
// prefer the current `accountState.balance` (`account_balance` row), and
|
|
160
|
+
// fall back to `truncatedAccountState.balance` (cursor baseline) when the
|
|
161
|
+
// former is the model default ZERO.
|
|
162
|
+
const accountBalance = isBase
|
|
163
|
+
? accountState?.balance
|
|
164
|
+
: accountState?.tokenBalances?.[asset.name]
|
|
165
|
+
if (accountBalance && (!isBase || !accountBalance.isZero)) {
|
|
166
|
+
return balance.add(accountBalance)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const truncatedBalance = isBase
|
|
170
|
+
? getTruncatedBaseBalance({ accountState })
|
|
171
|
+
: getTruncatedTokenBalance({ asset, accountState })
|
|
172
|
+
if (truncatedBalance) return balance.add(truncatedBalance)
|
|
173
|
+
|
|
174
|
+
console.warn('No absolute balance found', { assetName: asset.name, txLogSize: txLog.size })
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return balance
|
|
15
179
|
}
|
|
16
180
|
|
|
17
|
-
const
|
|
18
|
-
return
|
|
181
|
+
const getStaked = ({ accountState, asset }) => {
|
|
182
|
+
return accountState?.staking?.[asset.name]?.activeStakedBalance || asset.currency.ZERO
|
|
19
183
|
}
|
|
20
184
|
|
|
21
|
-
const
|
|
22
|
-
return (
|
|
23
|
-
|
|
185
|
+
const getStaking = ({ accountState, asset }) => {
|
|
186
|
+
return (accountState?.staking?.[asset.name]?.pendingBalance || asset.currency.ZERO).add(
|
|
187
|
+
accountState?.staking?.[asset.name]?.pendingDepositedBalance || asset.currency.ZERO
|
|
24
188
|
)
|
|
25
189
|
}
|
|
26
190
|
|
|
191
|
+
const getUnclaimedUndelegatedBalance = ({ accountState, asset }) => {
|
|
192
|
+
return accountState?.staking?.[asset.name]?.unclaimedUndelegatedBalance || asset.currency.ZERO
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const getCanClaimUndelegatedBalance = ({ accountState, asset }) =>
|
|
196
|
+
accountState?.staking?.[asset.name]?.canClaimUndelegatedBalance || false
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calculates amount received in staking txs from txLog
|
|
200
|
+
* Staking txs are counted based in the amount received in the tx (tx.coinAmount)
|
|
201
|
+
* It can be different from staked / unstaked / claimed requested amount)
|
|
202
|
+
*
|
|
203
|
+
* Unstaked tx.coinAmount - Staked tx.coinAmount + Claimed tx.coinAmount
|
|
204
|
+
*
|
|
205
|
+
* Depending on the asset, is what tx.coinAmount is:
|
|
206
|
+
*
|
|
207
|
+
* Ethereum: for unstake txs, it's unknown how much of it are rewards and unstaked received (full or partial)
|
|
208
|
+
* Polygon: rewards are transferred in stake and unstake txs
|
|
209
|
+
*/
|
|
210
|
+
export const getStakingHistoryBalance = ({ asset, txLog }) => {
|
|
211
|
+
let stakingHistoryBalance = asset.currency.ZERO
|
|
212
|
+
for (const tx of txLog) {
|
|
213
|
+
const successfulTx = tx.confirmations && !tx.failed
|
|
214
|
+
const txData = tx.data
|
|
215
|
+
|
|
216
|
+
// staking tx must has data value
|
|
217
|
+
if (!txData) continue
|
|
218
|
+
|
|
219
|
+
let { txAmount } = txData
|
|
220
|
+
if (successfulTx && (txAmount || txData.delegate)) {
|
|
221
|
+
// only staking txs have tx.data.txAmount set
|
|
222
|
+
|
|
223
|
+
if (txData.delegate && !txAmount) {
|
|
224
|
+
txAmount = txData.delegate // fix missing txAmount in stakeTx
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
stakingHistoryBalance = stakingHistoryBalance.add(asset.currency.defaultUnit(txAmount))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return stakingHistoryBalance
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const getSpendable = ({ asset, balance, txLog, unconfirmedReceived }) => {
|
|
235
|
+
const stakingHistoryBalance = getStakingHistoryBalance({ asset, txLog })
|
|
236
|
+
return balance.add(stakingHistoryBalance).sub(unconfirmedReceived)
|
|
237
|
+
}
|
|
238
|
+
|
|
27
239
|
/**
|
|
28
240
|
* Api method to return the balance based on either account state balances or tx history.
|
|
29
241
|
*
|
|
@@ -32,33 +244,79 @@ const getUnstaked = ({ accountState, asset }) => {
|
|
|
32
244
|
* @param accountState the account state when the balance is loaded from RPC
|
|
33
245
|
* @returns {{balance}|null} an object with the balance or null if the balance is unknown
|
|
34
246
|
*/
|
|
35
|
-
export const getBalancesFactory = ({ monitorType }) => {
|
|
247
|
+
export const getBalancesFactory = ({ monitorType, useAbsoluteBalance, rpcBalanceAssetNames }) => {
|
|
36
248
|
assert(monitorType, 'monitorType is required')
|
|
37
249
|
return ({ asset, txLog, accountState }) => {
|
|
38
250
|
const unconfirmedReceived = getUnconfirmedReceivedBalance({ asset, txLog })
|
|
39
251
|
const unconfirmedSent = getUnconfirmedSentBalance({ asset, txLog })
|
|
40
252
|
|
|
41
|
-
// balance from txLog / rpc is considered to be total
|
|
42
|
-
const balanceWithoutUnconfirmedSent =
|
|
43
|
-
monitorType === 'no-history' || isRpcBalanceAsset(asset)
|
|
44
|
-
? getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
|
|
45
|
-
: getBalanceFromTxLog({ txLog, asset })
|
|
46
|
-
|
|
47
253
|
const staked = getStaked({ asset, accountState })
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
254
|
+
const staking = getStaking({ asset, accountState })
|
|
255
|
+
|
|
256
|
+
const canClaimUndelegatedBalance = getCanClaimUndelegatedBalance({ accountState, asset })
|
|
257
|
+
|
|
258
|
+
const unclaimedUndelegatedBalance = getUnclaimedUndelegatedBalance({ asset, accountState })
|
|
259
|
+
|
|
260
|
+
const unstaking = canClaimUndelegatedBalance ? asset.currency.ZERO : unclaimedUndelegatedBalance
|
|
261
|
+
const unstaked = canClaimUndelegatedBalance ? unclaimedUndelegatedBalance : asset.currency.ZERO
|
|
262
|
+
|
|
263
|
+
let spendable
|
|
264
|
+
|
|
265
|
+
// Balance from accountState is considered total b/c is fetched from rpc
|
|
266
|
+
if (rpcBalanceAssetNames.includes(asset.name) || monitorType === 'no-history') {
|
|
267
|
+
// NOTE: Balances from a `no-history` monitor work like calls
|
|
268
|
+
// to `balanceOf`/`eth_getBalance` at the latest block tag,
|
|
269
|
+
// therefore there's no need to subtract `unconfirmedReceived`.
|
|
270
|
+
//
|
|
271
|
+
// Likewise, they do not compensate for pending sends, so we
|
|
272
|
+
// must manually subtract the `unconfirmedSent` to produce a
|
|
273
|
+
// pessimistic reading that's safe for consumers.
|
|
274
|
+
spendable = getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
|
|
275
|
+
} else {
|
|
276
|
+
const absoluteBalance =
|
|
277
|
+
useAbsoluteBalance && getAbsoluteBalance({ asset, txLog, accountState })
|
|
278
|
+
|
|
279
|
+
if (absoluteBalance) {
|
|
280
|
+
// NOTE: The returned `absoluteBalance` returns only confirmed
|
|
281
|
+
// values similar to the `'no-history'` case, so we need
|
|
282
|
+
// to reduce this amount by the `unconfirmedSent`.
|
|
283
|
+
spendable = absoluteBalance.sub(unconfirmedSent)
|
|
284
|
+
} else {
|
|
285
|
+
// Balance from txLog does not include staking rewards
|
|
286
|
+
// spendable and total are calculated differently based on staking txs
|
|
287
|
+
spendable = getSpendable({
|
|
288
|
+
asset,
|
|
289
|
+
balance: getBalanceFromTxLog({ txLog, asset }),
|
|
290
|
+
txLog,
|
|
291
|
+
unconfirmedReceived,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// A pending send already reflected in the account-state reading is double-counted
|
|
297
|
+
// via `unconfirmedSent`, making spendable transiently negative (bounded by
|
|
298
|
+
// `unconfirmedSent`). A negative beyond that bound is a real accounting bug.
|
|
299
|
+
if (spendable.isNegative && spendable.abs().gt(unconfirmedSent)) {
|
|
300
|
+
console.warn('unexpected negative spendable beyond pending sends', {
|
|
301
|
+
assetName: asset.name,
|
|
302
|
+
spendable: spendable.toBaseString(),
|
|
303
|
+
unconfirmedSent: unconfirmedSent.toBaseString(),
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Clamp before deriving `total` so the artifact doesn't drag it below staked balances.
|
|
308
|
+
spendable = spendable.clampLowerZero()
|
|
309
|
+
|
|
310
|
+
const total = spendable.add(staked).add(staking).add(unstaking).add(unstaked)
|
|
311
|
+
const stakeable = spendable
|
|
56
312
|
|
|
57
313
|
return {
|
|
58
314
|
// new
|
|
59
315
|
spendable,
|
|
60
316
|
total,
|
|
317
|
+
stakeable,
|
|
61
318
|
staked,
|
|
319
|
+
staking,
|
|
62
320
|
unstaking,
|
|
63
321
|
unstaked,
|
|
64
322
|
unconfirmedReceived,
|