@exodus/ethereum-api 8.59.2 → 8.60.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 +12 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +10 -3
- package/src/create-asset.js +2 -0
- package/src/staking/ethereum/service.js +15 -19
- package/src/staking/matic/matic-staking-utils.js +172 -1
- package/src/staking/matic/service.js +121 -58
- package/src/tx-log/get-optimistic-txlog-effects.js +66 -0
- package/src/tx-log-staking-processor/utils.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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
|
+
## [8.60.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.59.2...@exodus/ethereum-api@8.60.0) (2025-11-22)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: Bundle approve and delegate functions for Matic staking (With simulation) (#6707)
|
|
13
|
+
|
|
14
|
+
* feat: Default privacyRpcUrl (SERVO) (#6968)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
## [8.59.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.59.1...@exodus/ethereum-api@8.59.2) (2025-11-19)
|
|
7
19
|
|
|
8
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.60.0",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"type": "git",
|
|
68
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "93a853e1bd1595de3e72f7ba3ceb5826d018c71a"
|
|
71
71
|
}
|
|
@@ -61,7 +61,7 @@ export const resolveMonitorSettings = (
|
|
|
61
61
|
return { ...defaultResolution, monitorType: overrideMonitorType, serverUrl: overrideServerUrl }
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const stringifyPrivateTx = (tx) => {
|
|
64
|
+
export const stringifyPrivateTx = (tx) => {
|
|
65
65
|
assert(tx, 'expected tx')
|
|
66
66
|
if (tx instanceof Uint8Array) return `0x${Buffer.from(tx).toString('hex')}`
|
|
67
67
|
|
|
@@ -75,14 +75,21 @@ const broadcastPrivateBundleFactory =
|
|
|
75
75
|
({ privacyServer }) =>
|
|
76
76
|
async ({ txs }) => {
|
|
77
77
|
assert(Array.isArray(txs), 'txs must be an array')
|
|
78
|
-
|
|
78
|
+
assert(txs.length > 0, 'txs must be an non-empty array')
|
|
79
79
|
|
|
80
|
-
await privacyServer.sendRequest(
|
|
80
|
+
const sendBundleResult = await privacyServer.sendRequest(
|
|
81
81
|
privacyServer.buildRequest({
|
|
82
82
|
method: 'eth_sendBundle',
|
|
83
83
|
params: [{ txs: txs.map((tx) => stringifyPrivateTx(tx)) }],
|
|
84
84
|
})
|
|
85
85
|
)
|
|
86
|
+
if (typeof sendBundleResult === 'string') return { bundleHash: sendBundleResult }
|
|
87
|
+
|
|
88
|
+
const bundleHash = sendBundleResult?.bundleHash
|
|
89
|
+
if (typeof bundleHash === 'string') return { bundleHash }
|
|
90
|
+
|
|
91
|
+
console.warn('unexpected sendBundleResult shape, cannot determine bundle hash')
|
|
92
|
+
return null
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
export const createTransactionPrivacyFactory = ({ assetName, privacyRpcUrl }) => {
|
package/src/create-asset.js
CHANGED
|
@@ -69,6 +69,7 @@ export const createAssetFactory = ({
|
|
|
69
69
|
supportsCustomFees: defaultSupportsCustomFees = false,
|
|
70
70
|
useAbsoluteBalanceAndNonce = false,
|
|
71
71
|
delisted = false,
|
|
72
|
+
privacyRpcUrl: defaultPrivacyRpcUrl,
|
|
72
73
|
}) => {
|
|
73
74
|
assert(assetsList, 'assetsList is required')
|
|
74
75
|
assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
|
|
@@ -91,6 +92,7 @@ export const createAssetFactory = ({
|
|
|
91
92
|
supportsCustomFees: defaultSupportsCustomFees,
|
|
92
93
|
nfts: defaultNfts,
|
|
93
94
|
customTokens: defaultCustomTokens,
|
|
95
|
+
privacyRpcUrl: defaultPrivacyRpcUrl,
|
|
94
96
|
}
|
|
95
97
|
return (
|
|
96
98
|
{
|
|
@@ -60,7 +60,7 @@ export function createEthereumStakingService({
|
|
|
60
60
|
return { delegatorAddress, feeData }
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async function delegate({ walletAccount, amount, feeData, isDelegateAll }
|
|
63
|
+
async function delegate({ walletAccount, amount, feeData, isDelegateAll }) {
|
|
64
64
|
const asset = await getAsset(assetName)
|
|
65
65
|
const staking = getStakingApi(asset)
|
|
66
66
|
amount = amountToCurrency({ asset, amount })
|
|
@@ -230,9 +230,7 @@ export function createEthereumStakingService({
|
|
|
230
230
|
return undelegatePendingFee.add(undelegateFee)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
async function undelegate(
|
|
234
|
-
{ walletAccount, amount, feeData, waitForConfirmation = true } = Object.create(null)
|
|
235
|
-
) {
|
|
233
|
+
async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = true }) {
|
|
236
234
|
const asset = await getAsset(assetName)
|
|
237
235
|
const staking = getStakingApi(asset)
|
|
238
236
|
const minAmount = getMinAmount(asset)
|
|
@@ -312,7 +310,7 @@ export function createEthereumStakingService({
|
|
|
312
310
|
return txId
|
|
313
311
|
}
|
|
314
312
|
|
|
315
|
-
async function claimUndelegatedBalance({ walletAccount, feeData }
|
|
313
|
+
async function claimUndelegatedBalance({ walletAccount, feeData }) {
|
|
316
314
|
const asset = await getAsset(assetName)
|
|
317
315
|
const staking = getStakingApi(asset)
|
|
318
316
|
|
|
@@ -495,20 +493,18 @@ export function createEthereumStakingService({
|
|
|
495
493
|
}
|
|
496
494
|
}
|
|
497
495
|
|
|
498
|
-
async function prepareAndSendTx(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
} = Object.create(null)
|
|
511
|
-
) {
|
|
496
|
+
async function prepareAndSendTx({
|
|
497
|
+
asset,
|
|
498
|
+
walletAccount,
|
|
499
|
+
to,
|
|
500
|
+
amount,
|
|
501
|
+
txData: txInput,
|
|
502
|
+
gasPrice,
|
|
503
|
+
gasLimit,
|
|
504
|
+
tipGasPrice,
|
|
505
|
+
waitForConfirmation = false,
|
|
506
|
+
feeData,
|
|
507
|
+
}) {
|
|
512
508
|
const sendTxArgs = {
|
|
513
509
|
asset,
|
|
514
510
|
walletAccount,
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import NumberUnit from '@exodus/currency'
|
|
2
|
+
import { parseUnsignedTx } from '@exodus/ethereum-lib'
|
|
1
3
|
import assetsList, { asset as ethereum } from '@exodus/ethereum-meta'
|
|
2
4
|
import { Tx } from '@exodus/models'
|
|
5
|
+
import assert from 'minimalistic-assert'
|
|
3
6
|
|
|
7
|
+
import { decodePolygonStakingTxInputAmount } from '../../tx-log-staking-processor/utils.js'
|
|
4
8
|
import { MaticStakingApi } from './api.js'
|
|
5
9
|
|
|
6
10
|
// function selector for buyVoucher(uint256 _amount, uint256 _minSharesToMint)
|
|
7
|
-
const DELEGATE = '0x6ab15071'
|
|
11
|
+
export const DELEGATE = '0x6ab15071'
|
|
8
12
|
// function selector for sellVoucher_new(uint256 claimAmount, uint256 maximumSharesToBurn)
|
|
9
13
|
const UNDELEGATE = '0xc83ec04d'
|
|
10
14
|
// function selector for withdrawRewards()
|
|
@@ -76,3 +80,170 @@ export const getPolygonUndelegateTxInEthereumTxLog = (ethereumLogItems) => {
|
|
|
76
80
|
|
|
77
81
|
return polygonLogItems
|
|
78
82
|
}
|
|
83
|
+
|
|
84
|
+
// Build simulation transaction requests from unsigned transactions
|
|
85
|
+
export function createMaticStakeSimRequestsFromUnsigned({
|
|
86
|
+
asset,
|
|
87
|
+
unsignedTxApprove,
|
|
88
|
+
unsignedTxDelegate,
|
|
89
|
+
}) {
|
|
90
|
+
const build = (tx) => {
|
|
91
|
+
const parsed = parseUnsignedTx({ asset, unsignedTx: tx.unsignedTx })
|
|
92
|
+
const gasPriceUnit = tx.gasPrice
|
|
93
|
+
const chainId = tx.unsignedTx?.txData?.chainId
|
|
94
|
+
const from = tx.unsignedTx?.txMeta?.fromAddress
|
|
95
|
+
const to = tx.unsignedTx?.txMeta?.toAddress
|
|
96
|
+
|
|
97
|
+
assert(gasPriceUnit instanceof NumberUnit, 'expected NumberUnit gasPrice')
|
|
98
|
+
assert(Number.isInteger(chainId), 'expected integer chainId')
|
|
99
|
+
assert(typeof from === 'string' && from, 'expected string fromAddress')
|
|
100
|
+
assert(typeof to === 'string' && to, 'expected string toAddress')
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
chainId,
|
|
104
|
+
from,
|
|
105
|
+
to,
|
|
106
|
+
gas: `0x${parsed.gasLimit.toString(16)}`,
|
|
107
|
+
gasPrice: `0x${BigInt(gasPriceUnit.toBaseString()).toString(16)}`,
|
|
108
|
+
value: '0x0',
|
|
109
|
+
data: `0x${parsed.data?.toString('hex')}`,
|
|
110
|
+
nonce: `0x${parsed.nonce.toString(16)}`,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
approveTxRequest: build(unsignedTxApprove),
|
|
116
|
+
delegateTxRequest: build(unsignedTxDelegate),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function maticDelegateOptimisticSideEffectTxLogs({
|
|
121
|
+
asset, // Base asset
|
|
122
|
+
walletAccount,
|
|
123
|
+
feeAmount,
|
|
124
|
+
delegateTxId,
|
|
125
|
+
nonce,
|
|
126
|
+
estimatedDelegateGasLimit,
|
|
127
|
+
tipGasPrice,
|
|
128
|
+
transactionData,
|
|
129
|
+
date,
|
|
130
|
+
bundleId,
|
|
131
|
+
}) {
|
|
132
|
+
// Decode the amount from the delegate transaction data
|
|
133
|
+
// transactionData is already parsed from unsignedTx using parseUnsignedTx
|
|
134
|
+
let amount
|
|
135
|
+
try {
|
|
136
|
+
// Decode the contract call data - buyVoucher(uint256 _amount, uint256 _minSharesToMint)
|
|
137
|
+
const decodedAmount = decodePolygonStakingTxInputAmount({ data: { data: transactionData } })
|
|
138
|
+
// Convert to NumberUnit - the decoded amount is a string like '100000000'
|
|
139
|
+
amount = polygonCurrency.baseUnit(decodedAmount)
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(
|
|
142
|
+
'Could not decode MATIC delegate transaction data:',
|
|
143
|
+
delegateTxId,
|
|
144
|
+
e.message,
|
|
145
|
+
transactionData
|
|
146
|
+
)
|
|
147
|
+
return [] // Could not decode the amount
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const tokenAssetName = 'polygon'
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
assetName: tokenAssetName,
|
|
154
|
+
walletAccount,
|
|
155
|
+
txs: [
|
|
156
|
+
{
|
|
157
|
+
confirmations: 0,
|
|
158
|
+
feeAmount,
|
|
159
|
+
feeCoinName: asset.feeAsset.name,
|
|
160
|
+
selfSend: false,
|
|
161
|
+
to: MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
162
|
+
txId: delegateTxId,
|
|
163
|
+
data: {
|
|
164
|
+
gasLimit: estimatedDelegateGasLimit,
|
|
165
|
+
nonce,
|
|
166
|
+
tipGasPrice: tipGasPrice ? tipGasPrice.toBaseString() : undefined,
|
|
167
|
+
methodId: DELEGATE,
|
|
168
|
+
...(bundleId ? { bundleId } : null),
|
|
169
|
+
},
|
|
170
|
+
coinAmount: amount.abs().negate(),
|
|
171
|
+
coinName: tokenAssetName,
|
|
172
|
+
currencies: {
|
|
173
|
+
[tokenAssetName]: polygonCurrency,
|
|
174
|
+
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
175
|
+
},
|
|
176
|
+
date,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function maticDelegateSimulateTransactions({
|
|
184
|
+
asset,
|
|
185
|
+
unsignedTxApprove,
|
|
186
|
+
unsignedTxDelegate,
|
|
187
|
+
senderAddress,
|
|
188
|
+
revertOnSimulationError,
|
|
189
|
+
}) {
|
|
190
|
+
const { approveTxRequest, delegateTxRequest } = createMaticStakeSimRequestsFromUnsigned({
|
|
191
|
+
asset: asset.baseAsset,
|
|
192
|
+
unsignedTxApprove,
|
|
193
|
+
unsignedTxDelegate,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Simulate transactions
|
|
197
|
+
const simulationTxData = {
|
|
198
|
+
baseAssetName: asset.baseAsset.name,
|
|
199
|
+
origin: 'exodus-staking',
|
|
200
|
+
senderAddress,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const simulationResponse = await asset.baseAsset.api.web3.simulateTransactions({
|
|
204
|
+
...simulationTxData,
|
|
205
|
+
transactions: [approveTxRequest, delegateTxRequest],
|
|
206
|
+
})
|
|
207
|
+
// These checks are only for simulation issues, not validation of the transactions
|
|
208
|
+
if (simulationResponse?.warnings?.length || simulationResponse?.metadata?.humanReadableError) {
|
|
209
|
+
if (revertOnSimulationError) {
|
|
210
|
+
const err = new Error('StakingMaticSimulationError')
|
|
211
|
+
err.message = simulationResponse?.metadata?.humanReadableError
|
|
212
|
+
err.reason = `warnings: ${simulationResponse.warnings.join(', ')}`
|
|
213
|
+
err.hint = 'delegate-matic-simulation-error'
|
|
214
|
+
throw err
|
|
215
|
+
} else {
|
|
216
|
+
console.warn(
|
|
217
|
+
'Simulation for staking matic returned issues',
|
|
218
|
+
simulationResponse?.metadata?.humanReadableError
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// For validation of the transactions, we need to check if the simulation produced the expected effects
|
|
224
|
+
// hasApprove, hasSend, hasReceive entries must be present
|
|
225
|
+
const { balanceChanges = Object.create(null) } = simulationResponse || Object.create(null)
|
|
226
|
+
const asArray = (v) => (Array.isArray(v) ? v : [])
|
|
227
|
+
const hasApprove = asArray(balanceChanges.willApprove).length > 0
|
|
228
|
+
const hasSend = asArray(balanceChanges.willSend).length > 0
|
|
229
|
+
const hasReceive = asArray(balanceChanges.willReceive).length > 0
|
|
230
|
+
|
|
231
|
+
if (!(hasApprove && hasSend && hasReceive)) {
|
|
232
|
+
if (revertOnSimulationError) {
|
|
233
|
+
const missing = []
|
|
234
|
+
if (!hasApprove) missing.push('willApprove')
|
|
235
|
+
if (!hasSend) missing.push('willSend')
|
|
236
|
+
if (!hasReceive) missing.push('willReceive')
|
|
237
|
+
const err = new Error('StakingMaticSimulationResultsError')
|
|
238
|
+
err.message = 'Simulation did not produce expected effects'
|
|
239
|
+
err.reason = `missing balanceChanges: ${missing.join(', ')}`
|
|
240
|
+
err.hint = 'delegate-matic-simulation-results-error'
|
|
241
|
+
throw err
|
|
242
|
+
} else {
|
|
243
|
+
console.warn(
|
|
244
|
+
'Simulation for staking matic returned issues',
|
|
245
|
+
simulationResponse?.metadata?.humanReadableError
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -2,6 +2,7 @@ import { memoize } from '@exodus/basic-utils'
|
|
|
2
2
|
|
|
3
3
|
import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
|
|
4
4
|
import { getAggregateTransactionPricing } from '../../get-fee.js'
|
|
5
|
+
import { getOptimisticTxLogEffects } from '../../tx-log/get-optimistic-txlog-effects.js'
|
|
5
6
|
import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
|
|
6
7
|
import { stakingProviderClientFactory } from '../staking-provider-client.js'
|
|
7
8
|
import {
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
resolveFeeData as defaultResolveFeeData,
|
|
11
12
|
} from '../utils/index.js'
|
|
12
13
|
import { MaticStakingApi } from './api.js'
|
|
14
|
+
import { maticDelegateSimulateTransactions } from './matic-staking-utils.js'
|
|
13
15
|
|
|
14
16
|
const createStakingApiForFeeAsset = ({ feeAsset: { server } }) =>
|
|
15
17
|
new MaticStakingApi(undefined, undefined, server)
|
|
@@ -68,86 +70,146 @@ export function createPolygonStakingService({
|
|
|
68
70
|
return address.toLowerCase()
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Delegate MATIC tokens for staking.
|
|
75
|
+
* @param {walletAccount} params.walletAccount - The walletAccount for the delegator (required).
|
|
76
|
+
* @param {NumberUnit} params.amount - The amount of MATIC to delegate (required).
|
|
77
|
+
* @param {FeeData} params.feeData - Optional feeData to use for the transaction (optional, if not provided will be obtained from the asset client interface).
|
|
78
|
+
* @param {Boolean} params.revertOnSimulationError - Optional boolean to revert on simulation error (optional, default is true). Allows us to quickly react to simulator issues.
|
|
79
|
+
*/
|
|
80
|
+
async function delegate({ walletAccount, amount, feeData, revertOnSimulationError = true }) {
|
|
81
|
+
const [delegatorAddress, { asset, stakingApi }, resolvedFeeData] = await Promise.all([
|
|
73
82
|
getDelegatorAddress({ walletAccount }),
|
|
74
83
|
createStakingApi(),
|
|
84
|
+
resolveFeeData({ feeData }),
|
|
75
85
|
])
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
// for a delegation transaction, since `feeData` is expected
|
|
79
|
-
// to have been provided during `estimateDelegateTxFee`.
|
|
80
|
-
feeData = await resolveFeeData({ feeData })
|
|
87
|
+
feeData = resolvedFeeData
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
const baseNonce = await asset.baseAsset.getNonce({
|
|
90
|
+
asset,
|
|
91
|
+
fromAddress: delegatorAddress.toString(),
|
|
92
|
+
walletAccount,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const [txApproveData, txDelegateData] = await Promise.all([
|
|
96
|
+
stakingApi.approveStakeManager(amount),
|
|
97
|
+
stakingApi.delegate({ amount }),
|
|
98
|
+
])
|
|
83
99
|
|
|
84
|
-
const
|
|
85
|
-
|
|
100
|
+
const {
|
|
101
|
+
gasPrice,
|
|
102
|
+
gasLimit: approveGasLimit,
|
|
103
|
+
tipGasPrice,
|
|
104
|
+
} = await estimateTxFee({
|
|
86
105
|
from: delegatorAddress,
|
|
87
106
|
to: stakingApi.polygonContract.address,
|
|
88
107
|
txInput: txApproveData,
|
|
89
108
|
feeData,
|
|
90
109
|
})
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
to: stakingApi.polygonContract.address,
|
|
95
|
-
txData: txApproveData,
|
|
96
|
-
gasPrice,
|
|
97
|
-
gasLimit,
|
|
98
|
-
tipGasPrice,
|
|
110
|
+
|
|
111
|
+
// Estimate based on the average gas usage for the delegate transaction. 250000 is the average.
|
|
112
|
+
const { gasLimit: estimatedDelegateGasLimit } = await estimateDelegateTxFee({
|
|
99
113
|
feeData,
|
|
100
114
|
})
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// an increase in `gasPrice` could result in transaction
|
|
110
|
-
// failure.
|
|
111
|
-
//
|
|
112
|
-
// HACK: This will invaldiate the original estimate in calculated
|
|
113
|
-
// in `estimateDelegateTxFee`!
|
|
114
|
-
// TODO: Submit these transactions as an atomic bundle.
|
|
115
|
-
if (waitForConfirmation) {
|
|
116
|
-
feeData = await getLatestFeeData()
|
|
116
|
+
const createTxBaseArgs = {
|
|
117
|
+
asset: asset.baseAsset,
|
|
118
|
+
walletAccount,
|
|
119
|
+
fromAddress: delegatorAddress.toString(),
|
|
120
|
+
amount: asset.baseAsset.currency.ZERO,
|
|
121
|
+
tipGasPrice,
|
|
122
|
+
gasPrice,
|
|
117
123
|
}
|
|
124
|
+
const [unsignedTxApprove, unsignedTxDelegate] = await Promise.all([
|
|
125
|
+
asset.baseAsset.api.createTx({
|
|
126
|
+
...createTxBaseArgs,
|
|
127
|
+
toAddress: stakingApi.polygonContract.address,
|
|
128
|
+
txInput: txApproveData,
|
|
129
|
+
gasLimit: approveGasLimit,
|
|
130
|
+
nonce: baseNonce,
|
|
131
|
+
}),
|
|
132
|
+
asset.baseAsset.api.createTx({
|
|
133
|
+
...createTxBaseArgs,
|
|
134
|
+
toAddress: stakingApi.validatorShareContract.address,
|
|
135
|
+
txInput: txDelegateData,
|
|
136
|
+
gasLimit: estimatedDelegateGasLimit,
|
|
137
|
+
nonce: baseNonce + 1,
|
|
138
|
+
}),
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
await maticDelegateSimulateTransactions({
|
|
142
|
+
asset,
|
|
143
|
+
unsignedTxApprove,
|
|
144
|
+
unsignedTxDelegate,
|
|
145
|
+
senderAddress: delegatorAddress.toString(),
|
|
146
|
+
revertOnSimulationError,
|
|
147
|
+
})
|
|
118
148
|
|
|
119
|
-
|
|
149
|
+
// Sign transactions
|
|
150
|
+
const [approveSigned, delegateSigned] = await Promise.all([
|
|
151
|
+
assetClientInterface.signTransaction({
|
|
152
|
+
assetName: asset.baseAsset.name,
|
|
153
|
+
unsignedTx: unsignedTxApprove.unsignedTx,
|
|
154
|
+
walletAccount,
|
|
155
|
+
}),
|
|
156
|
+
assetClientInterface.signTransaction({
|
|
157
|
+
assetName: asset.baseAsset.name,
|
|
158
|
+
unsignedTx: unsignedTxDelegate.unsignedTx,
|
|
159
|
+
walletAccount,
|
|
160
|
+
}),
|
|
161
|
+
])
|
|
120
162
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
txInput: txDelegateData,
|
|
125
|
-
feeData,
|
|
126
|
-
}))
|
|
163
|
+
// Pre-compute txIDs
|
|
164
|
+
const approveTxId = `0x${approveSigned.txId.toString('hex')}`
|
|
165
|
+
const delegateTxId = `0x${delegateSigned.txId.toString('hex')}`
|
|
127
166
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
// TOOD: Why shouldn't we wait for confirmation here? Is it because we want to `notifyStaking`?
|
|
131
|
-
waitForConfirmation: false,
|
|
132
|
-
to: stakingApi.validatorShareContract.address,
|
|
133
|
-
txData: txDelegateData,
|
|
134
|
-
gasPrice,
|
|
135
|
-
gasLimit,
|
|
136
|
-
tipGasPrice,
|
|
137
|
-
feeData,
|
|
167
|
+
const bundleResponse = await asset.baseAsset.broadcastPrivateBundle({
|
|
168
|
+
txs: [approveSigned, delegateSigned].map(({ rawTx }) => rawTx),
|
|
138
169
|
})
|
|
170
|
+
const bundleHash = bundleResponse?.bundleHash
|
|
171
|
+
|
|
172
|
+
const { optimisticTxLogEffects: approveOptimisticTxLogEffects } =
|
|
173
|
+
await getOptimisticTxLogEffects({
|
|
174
|
+
asset: asset.baseAsset,
|
|
175
|
+
assetClientInterface,
|
|
176
|
+
fromAddress: delegatorAddress.toString(),
|
|
177
|
+
txId: approveTxId,
|
|
178
|
+
unsignedTx: unsignedTxApprove.unsignedTx,
|
|
179
|
+
walletAccount,
|
|
180
|
+
bundleId: bundleHash ?? undefined,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const { optimisticTxLogEffects: delegateOptimisticTxLogEffects } =
|
|
184
|
+
await getOptimisticTxLogEffects({
|
|
185
|
+
asset: asset.baseAsset,
|
|
186
|
+
assetClientInterface,
|
|
187
|
+
fromAddress: delegatorAddress.toString(),
|
|
188
|
+
txId: delegateTxId,
|
|
189
|
+
unsignedTx: unsignedTxDelegate.unsignedTx,
|
|
190
|
+
walletAccount,
|
|
191
|
+
bundleId: bundleHash ?? undefined,
|
|
192
|
+
})
|
|
193
|
+
// Combine optimistic effects from both transactions
|
|
194
|
+
const tokenOptimisticEffects = [
|
|
195
|
+
...approveOptimisticTxLogEffects,
|
|
196
|
+
...delegateOptimisticTxLogEffects,
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
for (const optimisticEffect of tokenOptimisticEffects) {
|
|
200
|
+
await assetClientInterface.updateTxLogAndNotify(optimisticEffect)
|
|
201
|
+
}
|
|
139
202
|
|
|
140
203
|
await stakingProvider.notifyStaking({
|
|
141
|
-
txId,
|
|
204
|
+
txId: delegateTxId,
|
|
142
205
|
asset: asset.name,
|
|
143
206
|
delegator: delegatorAddress,
|
|
144
207
|
amount: amount.toBaseString(),
|
|
145
208
|
})
|
|
146
|
-
|
|
147
|
-
return txId
|
|
209
|
+
return delegateTxId
|
|
148
210
|
}
|
|
149
211
|
|
|
150
|
-
async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = false }
|
|
212
|
+
async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = false }) {
|
|
151
213
|
feeData = await resolveOptionalFeeData({ feeData })
|
|
152
214
|
|
|
153
215
|
const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
|
|
@@ -177,7 +239,7 @@ export function createPolygonStakingService({
|
|
|
177
239
|
})
|
|
178
240
|
}
|
|
179
241
|
|
|
180
|
-
async function claimRewards({ walletAccount, feeData }
|
|
242
|
+
async function claimRewards({ walletAccount, feeData }) {
|
|
181
243
|
feeData = await resolveOptionalFeeData({ feeData })
|
|
182
244
|
|
|
183
245
|
const [delegatorAddress, { stakingApi }] = await Promise.all([
|
|
@@ -203,7 +265,7 @@ export function createPolygonStakingService({
|
|
|
203
265
|
})
|
|
204
266
|
}
|
|
205
267
|
|
|
206
|
-
async function claimUndelegatedBalance({ walletAccount, unbondNonce, feeData }
|
|
268
|
+
async function claimUndelegatedBalance({ walletAccount, unbondNonce, feeData }) {
|
|
207
269
|
feeData = await resolveOptionalFeeData({ feeData })
|
|
208
270
|
|
|
209
271
|
const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
|
|
@@ -301,7 +363,7 @@ export function createPolygonStakingService({
|
|
|
301
363
|
* This is just for displaying purposes and it's just an aproximation of the delegate gas cost,
|
|
302
364
|
* NOT the real fee cost
|
|
303
365
|
*/
|
|
304
|
-
async function estimateDelegateTxFee({ feeData } =
|
|
366
|
+
async function estimateDelegateTxFee({ feeData } = Object.create(null)) {
|
|
305
367
|
// approx gas limits
|
|
306
368
|
const { ethereum } = await assetClientInterface.getAssetsForNetwork({
|
|
307
369
|
baseAssetName: 'ethereum',
|
|
@@ -346,7 +408,8 @@ export function createPolygonStakingService({
|
|
|
346
408
|
gasPrice: DISABLE_BALANCE_CHECKS,
|
|
347
409
|
})
|
|
348
410
|
|
|
349
|
-
|
|
411
|
+
const fee = ethereum.api.getFee({ asset: ethereum, feeData, gasLimit, amount })
|
|
412
|
+
return { ...fee, gasLimit }
|
|
350
413
|
}
|
|
351
414
|
|
|
352
415
|
async function prepareAndSendTx({
|
|
@@ -358,7 +421,7 @@ export function createPolygonStakingService({
|
|
|
358
421
|
tipGasPrice,
|
|
359
422
|
waitForConfirmation = false,
|
|
360
423
|
feeData,
|
|
361
|
-
}
|
|
424
|
+
}) {
|
|
362
425
|
const { ethereum: asset } = await assetClientInterface.getAssetsForNetwork({
|
|
363
426
|
baseAssetName: 'ethereum',
|
|
364
427
|
})
|
|
@@ -4,6 +4,12 @@ import { isEthereumLikeToken, parseUnsignedTx } from '@exodus/ethereum-lib'
|
|
|
4
4
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
5
5
|
import assert from 'minimalistic-assert'
|
|
6
6
|
|
|
7
|
+
import { MaticStakingApi } from '../staking/matic/api.js'
|
|
8
|
+
import {
|
|
9
|
+
DELEGATE,
|
|
10
|
+
maticDelegateOptimisticSideEffectTxLogs,
|
|
11
|
+
} from '../staking/matic/matic-staking-utils.js'
|
|
12
|
+
|
|
7
13
|
// Returns the most competitively priced pending
|
|
8
14
|
// transaction from the `TxLog` for a given `nonce`.
|
|
9
15
|
//
|
|
@@ -50,6 +56,7 @@ export const getHighestIncentiveTxByNonce = async ({
|
|
|
50
56
|
export const getOptimisticTxLogEffects = async ({
|
|
51
57
|
asset,
|
|
52
58
|
assetClientInterface,
|
|
59
|
+
bundleId,
|
|
53
60
|
confirmations = 0,
|
|
54
61
|
date = SynchronizedTime.now(),
|
|
55
62
|
fromAddress,
|
|
@@ -108,6 +115,23 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
108
115
|
console.log('Attempting to replace a transaction using an insufficient fee!')
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
// Contains effects for smart-contract initiated token movements.
|
|
119
|
+
const methodOptimisticSideEffects = await operationTxLogSideEffects({
|
|
120
|
+
txToAddress: to,
|
|
121
|
+
methodId,
|
|
122
|
+
asset,
|
|
123
|
+
walletAccount,
|
|
124
|
+
feeAmount,
|
|
125
|
+
txId,
|
|
126
|
+
nonce,
|
|
127
|
+
gasLimit,
|
|
128
|
+
tipGasPrice: maybeTipGasPrice,
|
|
129
|
+
data,
|
|
130
|
+
date,
|
|
131
|
+
bundleId,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Fallback to basic logic that only handles transfers + fee decoding
|
|
111
135
|
const sharedProps = {
|
|
112
136
|
confirmations,
|
|
113
137
|
date,
|
|
@@ -122,6 +146,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
122
146
|
nonce,
|
|
123
147
|
...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
|
|
124
148
|
...(methodId ? { methodId } : null),
|
|
149
|
+
...(bundleId ? { bundleId } : null),
|
|
125
150
|
},
|
|
126
151
|
}
|
|
127
152
|
|
|
@@ -157,7 +182,48 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
157
182
|
},
|
|
158
183
|
],
|
|
159
184
|
},
|
|
185
|
+
...methodOptimisticSideEffects,
|
|
160
186
|
].filter(Boolean)
|
|
161
187
|
|
|
162
188
|
return { optimisticTxLogEffects, nonce }
|
|
163
189
|
}
|
|
190
|
+
|
|
191
|
+
const operationTxLogSideEffects = async ({
|
|
192
|
+
txToAddress,
|
|
193
|
+
methodId,
|
|
194
|
+
asset,
|
|
195
|
+
walletAccount,
|
|
196
|
+
feeAmount,
|
|
197
|
+
txId,
|
|
198
|
+
nonce,
|
|
199
|
+
gasLimit,
|
|
200
|
+
tipGasPrice,
|
|
201
|
+
data,
|
|
202
|
+
date,
|
|
203
|
+
bundleId,
|
|
204
|
+
}) => {
|
|
205
|
+
// Matic Delegate
|
|
206
|
+
if (
|
|
207
|
+
txToAddress?.toLowerCase() ===
|
|
208
|
+
MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR.toLowerCase() &&
|
|
209
|
+
methodId === DELEGATE
|
|
210
|
+
) {
|
|
211
|
+
return maticDelegateOptimisticSideEffectTxLogs({
|
|
212
|
+
asset,
|
|
213
|
+
walletAccount,
|
|
214
|
+
feeAmount,
|
|
215
|
+
delegateTxId: txId,
|
|
216
|
+
nonce,
|
|
217
|
+
estimatedDelegateGasLimit: gasLimit,
|
|
218
|
+
tipGasPrice,
|
|
219
|
+
transactionData: data,
|
|
220
|
+
date,
|
|
221
|
+
bundleId,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Add other officially supported operations here
|
|
226
|
+
// Then can fallback to simulation results (TODO)
|
|
227
|
+
// Then finally to the default basic logic
|
|
228
|
+
return []
|
|
229
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { asset as ethereum } from '@exodus/ethereum-meta'
|
|
2
2
|
import { asset as ethereumholesky } from '@exodus/ethereumholesky-meta'
|
|
3
3
|
|
|
4
|
-
import { EthereumStaking
|
|
4
|
+
import { EthereumStaking } from '../staking/ethereum/api.js'
|
|
5
|
+
import { isEthereumStakingTx } from '../staking/ethereum/staking-utils.js'
|
|
6
|
+
import { MaticStakingApi } from '../staking/matic/api.js'
|
|
5
7
|
|
|
6
8
|
const polygonStakingApi = new MaticStakingApi()
|
|
7
9
|
const ethereumStakingApi = new EthereumStaking(ethereum)
|