@exodus/ethereum-plugin 2.30.3 → 2.31.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 +19 -0
- package/package.json +5 -5
- package/src/index.js +2 -0
- package/src/staking/polygon/service.js +177 -69
- package/src/staking/polygon/staking-utils.js +108 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [2.31.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.30.4...@exodus/ethereum-plugin@2.31.0) (2026-05-19)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat(ethereum): add no-history receive logs for portfolio transfers (#7967)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [2.30.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.30.3...@exodus/ethereum-plugin@2.30.4) (2026-05-12)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* validate polygon unstake state before gas estimation ([#8002](https://github.com/ExodusMovement/assets/issues/8002)) ([f63a244](https://github.com/ExodusMovement/assets/commit/f63a2441868c6226fe14085c0f7d9587131e69e0))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
## [2.30.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.30.2...@exodus/ethereum-plugin@2.30.3) (2026-05-11)
|
|
7
26
|
|
|
8
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.0",
|
|
4
4
|
"description": "Ethereum plugin for Exodus SDK powered wallets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"@exodus/asset-lib": "^5.9.0",
|
|
25
25
|
"@exodus/basic-utils": "^3.0.1",
|
|
26
26
|
"@exodus/currency": "^6.0.1",
|
|
27
|
-
"@exodus/ethereum-api": "^8.
|
|
28
|
-
"@exodus/ethereum-lib": "^5.24.
|
|
27
|
+
"@exodus/ethereum-api": "^8.75.1",
|
|
28
|
+
"@exodus/ethereum-lib": "^5.24.1",
|
|
29
29
|
"@exodus/ethereum-meta": "^2.9.0",
|
|
30
30
|
"@exodus/ethereumjs": "^1.0.0",
|
|
31
31
|
"@exodus/safe-string": "^1.4.0",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@exodus/assets": "^11.4.0",
|
|
39
39
|
"@exodus/assets-testing": "^1.0.0",
|
|
40
40
|
"@exodus/crypto": "^1.0.0-rc.13",
|
|
41
|
-
"@exodus/evm-fork-testing": "^0.6.
|
|
41
|
+
"@exodus/evm-fork-testing": "^0.6.2",
|
|
42
42
|
"@exodus/models": "^13.0.0",
|
|
43
43
|
"@exodus/solidity-contract": "^1.3.0",
|
|
44
44
|
"@exodus/web3-ethereum-utils": "^4.7.5",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"type": "git",
|
|
54
54
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "ee5bf2d01055d8822c6421fa0ad4da84d4a33402"
|
|
57
57
|
}
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import assetsList from '@exodus/ethereum-meta'
|
|
|
5
5
|
import { tetherusdBlackListCheckFactory, usdcoinBlackListCheckFactory } from './blacklist-checks.js'
|
|
6
6
|
import { stakingConfiguration, stakingDependencies } from './staking.js'
|
|
7
7
|
|
|
8
|
+
export { POLYGON_STAKING_PREFLIGHT_ERROR_REASONS } from './staking/polygon/service.js'
|
|
9
|
+
|
|
8
10
|
const createAsset = createAssetFactory({
|
|
9
11
|
assetsList,
|
|
10
12
|
feeDataConfig: {
|
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
import { memoize } from '@exodus/basic-utils'
|
|
2
2
|
// eslint-disable-next-line @exodus/import/no-deprecated
|
|
3
|
-
import { isNumberUnit } from '@exodus/currency'
|
|
3
|
+
import NumberUnit, { isNumberUnit } from '@exodus/currency'
|
|
4
4
|
import {
|
|
5
5
|
estimateGasLimit,
|
|
6
|
+
EthLikeError,
|
|
7
|
+
EVM_ERROR_TYPES,
|
|
6
8
|
getAggregateTransactionPricing,
|
|
7
|
-
getOptimisticTxLogEffects,
|
|
8
9
|
scaleGasLimitEstimate,
|
|
9
10
|
stakingProviderClientFactory,
|
|
10
11
|
} from '@exodus/ethereum-api'
|
|
12
|
+
import { safeString } from '@exodus/safe-string'
|
|
11
13
|
|
|
12
14
|
import { maticDelegateSimulateTransactions } from './matic-staking-utils.js'
|
|
15
|
+
import { getPendingUndelegateAmountFromEthereumTxLog } from './staking-utils.js'
|
|
16
|
+
|
|
17
|
+
export const POLYGON_STAKING_PREFLIGHT_ERROR_REASONS = {
|
|
18
|
+
polygonUnstakeAmountInvalid: {
|
|
19
|
+
reason: safeString`polygon unstake amount invalid`,
|
|
20
|
+
type: EVM_ERROR_TYPES.PREFLIGHT_VALIDATION,
|
|
21
|
+
},
|
|
22
|
+
polygonUnstakeAmountUnavailable: {
|
|
23
|
+
reason: safeString`polygon unstake amount unavailable`,
|
|
24
|
+
type: EVM_ERROR_TYPES.PREFLIGHT_VALIDATION,
|
|
25
|
+
},
|
|
26
|
+
polygonUnstakeClaimNonceInvalid: {
|
|
27
|
+
reason: safeString`polygon unstake claim nonce invalid`,
|
|
28
|
+
type: EVM_ERROR_TYPES.PREFLIGHT_VALIDATION,
|
|
29
|
+
},
|
|
30
|
+
polygonUnstakeClaimUnavailable: {
|
|
31
|
+
reason: safeString`polygon unstake claim unavailable`,
|
|
32
|
+
type: EVM_ERROR_TYPES.PREFLIGHT_VALIDATION,
|
|
33
|
+
},
|
|
34
|
+
polygonUnstakeClaimNotReady: {
|
|
35
|
+
reason: safeString`polygon unstake claim not ready`,
|
|
36
|
+
type: EVM_ERROR_TYPES.PREFLIGHT_VALIDATION,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
13
39
|
|
|
14
40
|
export function stakingServiceFactory({ assetClientInterface, server: _server, stakingServer }) {
|
|
15
41
|
const stakingProvider = stakingProviderClientFactory()
|
|
@@ -66,6 +92,105 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
|
|
|
66
92
|
return address.toLowerCase()
|
|
67
93
|
}
|
|
68
94
|
|
|
95
|
+
const getPendingUndelegateAmount = async ({ walletAccount, asset }) => {
|
|
96
|
+
if (typeof assetClientInterface.getTxLog !== 'function') return asset.currency.ZERO
|
|
97
|
+
|
|
98
|
+
const ethereumTxLog = await assetClientInterface.getTxLog({ assetName, walletAccount })
|
|
99
|
+
return getPendingUndelegateAmountFromEthereumTxLog({
|
|
100
|
+
ethereumTxLog,
|
|
101
|
+
currency: asset.currency,
|
|
102
|
+
validatorShareContract: stakingServer.validatorShareContract,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const getUndelegateParams = async ({ walletAccount, amount, delegatorAddress, asset }) => {
|
|
107
|
+
if (!(amount instanceof NumberUnit) || !amount.gt(asset.currency.ZERO)) {
|
|
108
|
+
throw new EthLikeError({
|
|
109
|
+
message: 'Undelegate amount must be positive',
|
|
110
|
+
errorReasonInfo: POLYGON_STAKING_PREFLIGHT_ERROR_REASONS.polygonUnstakeAmountInvalid,
|
|
111
|
+
hint: safeString`polygonStaking:undelegate:amount`,
|
|
112
|
+
baseAssetName: asset.baseAsset.name,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const [delegatedBalance, pendingUndelegateAmount] = await Promise.all([
|
|
117
|
+
stakingServer.getTotalStake(delegatorAddress),
|
|
118
|
+
getPendingUndelegateAmount({ walletAccount, asset }),
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
if (delegatedBalance.isZero) {
|
|
122
|
+
throw new EthLikeError({
|
|
123
|
+
message: 'No active Polygon stake to undelegate',
|
|
124
|
+
errorReasonInfo: POLYGON_STAKING_PREFLIGHT_ERROR_REASONS.polygonUnstakeAmountUnavailable,
|
|
125
|
+
hint: safeString`polygonStaking:undelegate:activeStake`,
|
|
126
|
+
baseAssetName: asset.baseAsset.name,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const availableActiveStake = delegatedBalance.sub(pendingUndelegateAmount)
|
|
131
|
+
|
|
132
|
+
if (amount.gt(availableActiveStake)) {
|
|
133
|
+
throw new EthLikeError({
|
|
134
|
+
message: 'Undelegate amount exceeds active Polygon stake',
|
|
135
|
+
errorReasonInfo: POLYGON_STAKING_PREFLIGHT_ERROR_REASONS.polygonUnstakeAmountUnavailable,
|
|
136
|
+
hint: safeString`polygonStaking:undelegate:availableStake`,
|
|
137
|
+
baseAssetName: asset.baseAsset.name,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { amount }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const getClaimUndelegatedParams = async ({ asset, delegatorAddress, unbondNonce }) => {
|
|
145
|
+
const { currency } = asset
|
|
146
|
+
|
|
147
|
+
if (!Number.isInteger(unbondNonce) || unbondNonce <= 0) {
|
|
148
|
+
throw new EthLikeError({
|
|
149
|
+
message: 'Polygon unstake claim nonce is invalid',
|
|
150
|
+
errorReasonInfo: POLYGON_STAKING_PREFLIGHT_ERROR_REASONS.polygonUnstakeClaimNonceInvalid,
|
|
151
|
+
hint: safeString`polygonStaking:claimUnstaked:nonce`,
|
|
152
|
+
baseAssetName: asset.baseAsset.name,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const [{ shares, withdrawEpoch }, withdrawalDelay, currentEpoch, withdrawExchangeRate] =
|
|
157
|
+
await Promise.all([
|
|
158
|
+
stakingServer.getUnboundInfo(delegatorAddress, unbondNonce),
|
|
159
|
+
stakingServer.getWithdrawalDelay(),
|
|
160
|
+
stakingServer.getCurrentCheckpoint(),
|
|
161
|
+
stakingServer.getWithdrawExchangeRate(),
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
if (shares.isZero()) {
|
|
165
|
+
throw new EthLikeError({
|
|
166
|
+
message: 'No Polygon unstake claim available',
|
|
167
|
+
errorReasonInfo: POLYGON_STAKING_PREFLIGHT_ERROR_REASONS.polygonUnstakeClaimUnavailable,
|
|
168
|
+
hint: safeString`polygonStaking:claimUnstaked:shares`,
|
|
169
|
+
baseAssetName: asset.baseAsset.name,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (withdrawEpoch.add(withdrawalDelay).gt(currentEpoch)) {
|
|
174
|
+
throw new EthLikeError({
|
|
175
|
+
message: 'Polygon unstake claim is not ready',
|
|
176
|
+
errorReasonInfo: POLYGON_STAKING_PREFLIGHT_ERROR_REASONS.polygonUnstakeClaimNotReady,
|
|
177
|
+
hint: safeString`polygonStaking:claimUnstaked:withdrawalDelay`,
|
|
178
|
+
baseAssetName: asset.baseAsset.name,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
unclaimedUndelegatedBalance: calculateUnclaimedTokens({
|
|
184
|
+
currency,
|
|
185
|
+
exchangeRatePrecision: stakingServer.EXCHANGE_RATE_PRECISION,
|
|
186
|
+
withdrawExchangeRate,
|
|
187
|
+
shares,
|
|
188
|
+
canClaimUndelegatedBalance: true,
|
|
189
|
+
isUndelegateInProgress: false,
|
|
190
|
+
}),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
69
194
|
async function approveDelegateAmount({ walletAccount, amount, feeData } = {}) {
|
|
70
195
|
feeData = await resolveOptionalFeeData({ feeData })
|
|
71
196
|
|
|
@@ -174,59 +299,22 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
|
|
|
174
299
|
revertOnSimulationError,
|
|
175
300
|
})
|
|
176
301
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Pre-compute txIDs
|
|
192
|
-
const approveTxId = `0x${approveSigned.txId.toString('hex')}`
|
|
193
|
-
const delegateTxId = `0x${delegateSigned.txId.toString('hex')}`
|
|
194
|
-
|
|
195
|
-
const bundleResponse = await feeAsset.broadcastPrivateBundle({
|
|
196
|
-
txs: [approveSigned, delegateSigned].map(({ rawTx }) => rawTx),
|
|
302
|
+
const { txIds } = await asset.baseAsset.sendPrivateBundle({
|
|
303
|
+
unsignedTxBundle: [
|
|
304
|
+
{
|
|
305
|
+
asset: feeAsset,
|
|
306
|
+
unsignedTx: unsignedTxApprove.unsignedTx,
|
|
307
|
+
walletAccount,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
asset: feeAsset,
|
|
311
|
+
unsignedTx: unsignedTxDelegate.unsignedTx,
|
|
312
|
+
walletAccount,
|
|
313
|
+
},
|
|
314
|
+
],
|
|
197
315
|
})
|
|
198
|
-
const bundleHash = bundleResponse?.bundleHash
|
|
199
|
-
|
|
200
|
-
const { optimisticTxLogEffects: approveOptimisticTxLogEffects } =
|
|
201
|
-
await getOptimisticTxLogEffects({
|
|
202
|
-
asset: feeAsset,
|
|
203
|
-
assetClientInterface,
|
|
204
|
-
fromAddress: delegatorAddress.toString(),
|
|
205
|
-
txId: approveTxId,
|
|
206
|
-
unsignedTx: unsignedTxApprove.unsignedTx,
|
|
207
|
-
walletAccount,
|
|
208
|
-
bundleId: bundleHash ?? undefined,
|
|
209
|
-
})
|
|
210
316
|
|
|
211
|
-
const
|
|
212
|
-
await getOptimisticTxLogEffects({
|
|
213
|
-
asset: feeAsset,
|
|
214
|
-
assetClientInterface,
|
|
215
|
-
fromAddress: delegatorAddress.toString(),
|
|
216
|
-
txId: delegateTxId,
|
|
217
|
-
unsignedTx: unsignedTxDelegate.unsignedTx,
|
|
218
|
-
walletAccount,
|
|
219
|
-
bundleId: bundleHash ?? undefined,
|
|
220
|
-
})
|
|
221
|
-
// Combine optimistic effects from both transactions
|
|
222
|
-
const tokenOptimisticEffects = [
|
|
223
|
-
...approveOptimisticTxLogEffects,
|
|
224
|
-
...delegateOptimisticTxLogEffects,
|
|
225
|
-
]
|
|
226
|
-
|
|
227
|
-
for (const optimisticEffect of tokenOptimisticEffects) {
|
|
228
|
-
await assetClientInterface.updateTxLogAndNotify(optimisticEffect)
|
|
229
|
-
}
|
|
317
|
+
const delegateTxId = txIds[txIds.length - 1]
|
|
230
318
|
|
|
231
319
|
await stakingProvider.notifyStaking({
|
|
232
320
|
txId: delegateTxId,
|
|
@@ -247,7 +335,14 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
|
|
|
247
335
|
|
|
248
336
|
amount = amountToCurrency({ asset, amount })
|
|
249
337
|
|
|
250
|
-
const
|
|
338
|
+
const undelegateParams = await getUndelegateParams({
|
|
339
|
+
walletAccount,
|
|
340
|
+
amount,
|
|
341
|
+
delegatorAddress,
|
|
342
|
+
asset,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const txUndelegateData = await stakingServer.undelegate(undelegateParams)
|
|
251
346
|
const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
|
|
252
347
|
from: delegatorAddress.toLowerCase(),
|
|
253
348
|
to: stakingServer.validatorShareContract.address,
|
|
@@ -297,17 +392,10 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
|
|
|
297
392
|
getStakeAssets(),
|
|
298
393
|
])
|
|
299
394
|
|
|
300
|
-
const {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
const { unclaimedUndelegatedBalance } = await getUnstakedUnclaimedInfo({
|
|
307
|
-
stakingServer,
|
|
308
|
-
currency,
|
|
309
|
-
delegator: delegatorAddress,
|
|
310
|
-
...unstakedClaimInfo,
|
|
395
|
+
const { unclaimedUndelegatedBalance } = await getClaimUndelegatedParams({
|
|
396
|
+
asset,
|
|
397
|
+
delegatorAddress,
|
|
398
|
+
unbondNonce,
|
|
311
399
|
})
|
|
312
400
|
|
|
313
401
|
const txClaimUndelegatedData = await stakingServer.claimUndelegatedBalance({ unbondNonce })
|
|
@@ -366,6 +454,26 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
|
|
|
366
454
|
args = { ...args, amount: amountToCurrency({ asset, amount }) }
|
|
367
455
|
}
|
|
368
456
|
|
|
457
|
+
if (operation === 'undelegate') {
|
|
458
|
+
args = {
|
|
459
|
+
...args,
|
|
460
|
+
...(await getUndelegateParams({
|
|
461
|
+
walletAccount,
|
|
462
|
+
amount: args.amount,
|
|
463
|
+
delegatorAddress,
|
|
464
|
+
asset,
|
|
465
|
+
})),
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (operation === 'claimUndelegatedBalance') {
|
|
470
|
+
await getClaimUndelegatedParams({
|
|
471
|
+
asset,
|
|
472
|
+
delegatorAddress,
|
|
473
|
+
unbondNonce: args.unbondNonce,
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
|
|
369
477
|
const operationTxData = await delegateOperation({ ...args, walletAccount })
|
|
370
478
|
const { fee } = await estimateTxFee({
|
|
371
479
|
from: delegatorAddress,
|
|
@@ -443,7 +551,7 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
|
|
|
443
551
|
feeData,
|
|
444
552
|
} = {}) {
|
|
445
553
|
const { feeAsset } = await getStakeAssets()
|
|
446
|
-
const
|
|
554
|
+
const { unsignedTx } = await feeAsset.api.createTx({
|
|
447
555
|
asset: feeAsset,
|
|
448
556
|
walletAccount,
|
|
449
557
|
address: to,
|
|
@@ -454,9 +562,9 @@ export function stakingServiceFactory({ assetClientInterface, server: _server, s
|
|
|
454
562
|
tipGasPrice,
|
|
455
563
|
waitForConfirmation,
|
|
456
564
|
feeData,
|
|
457
|
-
}
|
|
565
|
+
})
|
|
458
566
|
|
|
459
|
-
const { txId } = await feeAsset.api.sendTx(
|
|
567
|
+
const { txId } = await feeAsset.api.sendTx({ asset: feeAsset, walletAccount, unsignedTx })
|
|
460
568
|
|
|
461
569
|
return txId
|
|
462
570
|
}
|
|
@@ -536,7 +644,7 @@ async function getUnstakedUnclaimedInfo({
|
|
|
536
644
|
const { withdrawEpoch, shares } = await stakingServer.getUnboundInfo(delegator, unbondNonce)
|
|
537
645
|
const exchangeRatePrecision = stakingServer.EXCHANGE_RATE_PRECISION
|
|
538
646
|
const isUndelegateInProgress =
|
|
539
|
-
!withdrawEpoch.isZero() && withdrawEpoch.add(withdrawalDelay).
|
|
647
|
+
!withdrawEpoch.isZero() && withdrawEpoch.add(withdrawalDelay).gt(currentEpoch)
|
|
540
648
|
const isUndelegatedBalanceClaimable = canClaimUndelegatedBalance({
|
|
541
649
|
shares,
|
|
542
650
|
withdrawEpoch,
|
|
@@ -1,8 +1,116 @@
|
|
|
1
|
+
import { getMethodIdFromEthTx, isConfirmedTxInLog, isPendingTxInLog } from '@exodus/ethereum-lib'
|
|
1
2
|
import assert from 'minimalistic-assert'
|
|
2
3
|
|
|
3
4
|
import { mainnetContracts, methodIds as methodIds_ } from './contracts/index.js'
|
|
4
5
|
import { txFiltersFactory } from './tx-filters/index.js'
|
|
5
6
|
|
|
7
|
+
// Every ethereum txLog writer stores the nonce at `tx.data.nonce` as an
|
|
8
|
+
// integer (the optimistic outgoing-tx effects, the clarity + monitor-utils
|
|
9
|
+
// server-tx normalisers, and the staking optimistic side-effect builders
|
|
10
|
+
// all converge on this shape). There is no top-level `tx.nonce` field on
|
|
11
|
+
// the Tx model, so we only read from `tx.data.nonce`.
|
|
12
|
+
const getTxNonce = (tx) => (Number.isInteger(tx.data?.nonce) ? tx.data.nonce : undefined)
|
|
13
|
+
|
|
14
|
+
// Identifies txs sent by our wallet. Every ethereum txLog writer produces
|
|
15
|
+
// `from: []` for an outgoing tx: the optimistic outgoing-tx effects set it
|
|
16
|
+
// explicitly, both server-tx normalisers (clarity-utils and monitor-utils)
|
|
17
|
+
// collapse to `from: []` when `serverTx.from === ourWalletAddress`, and the
|
|
18
|
+
// staking optimistic side-effect builders omit `from` so the Tx constructor
|
|
19
|
+
// defaults it to `[]`. Incoming txs always carry `from: [theirAddress]`. We
|
|
20
|
+
// do not check the element against `ourAddress` because no writer produces
|
|
21
|
+
// `from: [ourAddress]` for an outgoing tx — the empty-array convention is
|
|
22
|
+
// the contract.
|
|
23
|
+
const isOurOutgoingTx = (tx) => Array.isArray(tx.from) && tx.from.length === 0
|
|
24
|
+
|
|
25
|
+
const isPolygonUndelegateTx = ({ tx, validatorShareAddress, methodIds }) =>
|
|
26
|
+
tx.to?.toLowerCase() === validatorShareAddress &&
|
|
27
|
+
getMethodIdFromEthTx(tx, methodIds.UNDELEGATE) === methodIds.UNDELEGATE.toLowerCase()
|
|
28
|
+
|
|
29
|
+
const decodePendingUndelegateAmount = ({ tx, currency, validatorShareContract }) => {
|
|
30
|
+
const {
|
|
31
|
+
values: [pendingUndelegateAmount],
|
|
32
|
+
} = validatorShareContract.decodeInput(tx.data.data)
|
|
33
|
+
|
|
34
|
+
return currency.baseUnit(pendingUndelegateAmount)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const getPendingUndelegateAmountFromEthereumTxLog = ({
|
|
38
|
+
ethereumTxLog = [],
|
|
39
|
+
currency,
|
|
40
|
+
validatorShareContract,
|
|
41
|
+
methodIds = methodIds_,
|
|
42
|
+
}) => {
|
|
43
|
+
const validatorShareAddress = validatorShareContract.address.toLowerCase()
|
|
44
|
+
const txsByNonce = new Map()
|
|
45
|
+
let pendingAmount = currency.ZERO
|
|
46
|
+
|
|
47
|
+
// Only our own txs can affect our delegated stake, so filter early. Bucket
|
|
48
|
+
// by nonce alone because all surviving txs share a single sender and only
|
|
49
|
+
// one tx per nonce can ever mine. The txLog is already scoped to the
|
|
50
|
+
// active wallet account by assetClientInterface.getTxLog, so within that
|
|
51
|
+
// scope `from: []` uniquely identifies our outgoing txs.
|
|
52
|
+
for (const tx of ethereumTxLog ?? []) {
|
|
53
|
+
if (!(isPendingTxInLog(tx) || isConfirmedTxInLog(tx))) continue
|
|
54
|
+
if (!isOurOutgoingTx(tx)) continue
|
|
55
|
+
|
|
56
|
+
const nonce = getTxNonce(tx)
|
|
57
|
+
if (!Number.isInteger(nonce)) continue
|
|
58
|
+
|
|
59
|
+
const txs = txsByNonce.get(nonce) || []
|
|
60
|
+
txs.push(tx)
|
|
61
|
+
txsByNonce.set(nonce, txs)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const txs of txsByNonce.values()) {
|
|
65
|
+
// Confirmed same-nonce txs mean the pending attempt can no longer execute.
|
|
66
|
+
if (txs.some(isConfirmedTxInLog)) continue
|
|
67
|
+
|
|
68
|
+
const pendingUndelegateTxs = txs.filter(
|
|
69
|
+
(tx) =>
|
|
70
|
+
isPendingTxInLog(tx) && isPolygonUndelegateTx({ tx, validatorShareAddress, methodIds })
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// If the bucket has no pending undelegate, nothing here can reduce our
|
|
74
|
+
// stake — whatever else lives at this nonce (a cancel, an unrelated tx)
|
|
75
|
+
// is someone else's problem.
|
|
76
|
+
//
|
|
77
|
+
// If there IS a pending undelegate, reserve it even when it shares the
|
|
78
|
+
// bucket with a cancel/replacement. The cancel almost always wins
|
|
79
|
+
// (higher fee is a precondition for tx-acceleration), but in the rare
|
|
80
|
+
// race where the original undelegate mines first we'd otherwise let the
|
|
81
|
+
// user broadcast a follow-up unstake that would revert on-chain. The
|
|
82
|
+
// cost of being conservative is a ~1-block UX delay after a successful
|
|
83
|
+
// cancel until clarity-monitor evicts the dead undelegate from the
|
|
84
|
+
// txLog.
|
|
85
|
+
if (pendingUndelegateTxs.length === 0) continue
|
|
86
|
+
|
|
87
|
+
// Reserve the largest decoded amount in the bucket. Same-nonce attempts
|
|
88
|
+
// are racing for the same on-chain slot, so at most one will mine — but we
|
|
89
|
+
// don't know which. Picking the max means the preflight stays safe even
|
|
90
|
+
// if an accelerated/replaced tx somehow carries a larger claim than the
|
|
91
|
+
// original, and it preserves the known-good amount when one of several
|
|
92
|
+
// attempts has corrupt input data.
|
|
93
|
+
let amountForNonce
|
|
94
|
+
|
|
95
|
+
for (const tx of pendingUndelegateTxs) {
|
|
96
|
+
try {
|
|
97
|
+
const amount = decodePendingUndelegateAmount({ tx, currency, validatorShareContract })
|
|
98
|
+
if (!amountForNonce || amount.gt(amountForNonce)) {
|
|
99
|
+
amountForNonce = amount
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.warn('Could not decode pending Polygon unstake amount', tx.txId, err)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!amountForNonce) continue
|
|
107
|
+
|
|
108
|
+
pendingAmount = pendingAmount.add(amountForNonce)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return pendingAmount
|
|
112
|
+
}
|
|
113
|
+
|
|
6
114
|
/**
|
|
7
115
|
* Used in ethereum-hooks to extract the final tx amount and the staking type
|
|
8
116
|
*
|