@exodus/ethereum-plugin 2.23.0 → 2.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +5 -5
- package/src/staking/polygon/api.js +26 -2
- package/src/staking/polygon/matic-staking-utils.js +107 -0
- package/src/staking/polygon/service.js +309 -156
- package/src/staking.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.23.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.23.0...@exodus/ethereum-plugin@2.23.1) (2026-02-09)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: monitors should check rpc before dropping (#7277)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
## [2.23.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.22.0...@exodus/ethereum-plugin@2.23.0) (2026-01-14)
|
|
7
17
|
|
|
8
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-plugin",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.1",
|
|
4
4
|
"description": "Ethereum plugin for Exodus SDK powered wallets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
"lint:fix": "yarn lint --fix"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@exodus/basic-utils": "^3.0.1",
|
|
24
25
|
"@exodus/currency": "^6.0.1",
|
|
25
|
-
"@exodus/ethereum-api": "^8.64.
|
|
26
|
+
"@exodus/ethereum-api": "^8.64.4",
|
|
26
27
|
"@exodus/ethereum-lib": "^5.21.0",
|
|
27
28
|
"@exodus/ethereum-meta": "^2.9.0",
|
|
28
29
|
"@exodus/ethereumjs": "^1.0.0",
|
|
@@ -33,11 +34,10 @@
|
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@exodus/assets": "^11.4.0",
|
|
35
36
|
"@exodus/assets-testing": "^1.0.0",
|
|
36
|
-
"@exodus/basic-utils": "^3.0.1",
|
|
37
37
|
"@exodus/crypto": "^1.0.0-rc.13",
|
|
38
38
|
"@exodus/evm-fork-testing": "^0.4.0",
|
|
39
39
|
"@exodus/models": "^12.13.0",
|
|
40
|
-
"@exodus/web3-ethereum-utils": "^4.7.
|
|
40
|
+
"@exodus/web3-ethereum-utils": "^4.7.2",
|
|
41
41
|
"delay": "^4.0.1",
|
|
42
42
|
"ms": "^2.1.1"
|
|
43
43
|
},
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"type": "git",
|
|
50
50
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "ebcc3fe022dfc1e21e3ccdc00faf0769e8fb3092"
|
|
53
53
|
}
|
|
@@ -115,6 +115,30 @@ class StakingServer {
|
|
|
115
115
|
return this.asset.currency.baseUnit(liquidRewards)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
getTotalRewards = async (address) => {
|
|
119
|
+
// use external everstake API to get total rewards
|
|
120
|
+
const EVERSTAKE_API_URL = 'https://polygon-clarity.a.exodus.io/everstake-rewards'
|
|
121
|
+
const url = new URL(EVERSTAKE_API_URL)
|
|
122
|
+
|
|
123
|
+
const options = {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
address,
|
|
128
|
+
}),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch(url.toString(), options)
|
|
133
|
+
const res = await response.json()
|
|
134
|
+
if (res === 'address' || !res?.rewards) return this.asset.currency.ZERO // address not found
|
|
135
|
+
return this.asset.currency.baseUnit(res.rewards)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn('Error fetching Polygon total rewards:', error)
|
|
138
|
+
return this.asset.currency.ZERO
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
getTotalStake = async (address) => {
|
|
119
143
|
const stakeInfo = await this.#callReadFunctionContract(
|
|
120
144
|
this.validatorShareContract,
|
|
@@ -182,5 +206,5 @@ class StakingServer {
|
|
|
182
206
|
}
|
|
183
207
|
}
|
|
184
208
|
|
|
185
|
-
export const stakingServerFactory = ({
|
|
186
|
-
new StakingServer({ asset, contracts, server })
|
|
209
|
+
export const stakingServerFactory = ({ assetName, currency, contracts, server }) =>
|
|
210
|
+
new StakingServer({ asset: { name: assetName, currency }, contracts, server })
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import NumberUnit from '@exodus/currency'
|
|
2
|
+
import { parseUnsignedTx } from '@exodus/ethereum-lib'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
// Build simulation transaction requests from unsigned transactions
|
|
6
|
+
export function createMaticStakeSimRequestsFromUnsigned({
|
|
7
|
+
asset,
|
|
8
|
+
unsignedTxApprove,
|
|
9
|
+
unsignedTxDelegate,
|
|
10
|
+
}) {
|
|
11
|
+
const build = (tx) => {
|
|
12
|
+
const parsed = parseUnsignedTx({ asset, unsignedTx: tx.unsignedTx })
|
|
13
|
+
const gasPriceUnit = tx.gasPrice
|
|
14
|
+
const chainId = tx.unsignedTx?.txData?.chainId
|
|
15
|
+
const from = tx.unsignedTx?.txMeta?.fromAddress
|
|
16
|
+
const to = tx.unsignedTx?.txMeta?.toAddress
|
|
17
|
+
|
|
18
|
+
assert(gasPriceUnit instanceof NumberUnit, 'expected NumberUnit gasPrice')
|
|
19
|
+
assert(Number.isInteger(chainId), 'expected integer chainId')
|
|
20
|
+
assert(typeof from === 'string' && from, 'expected string fromAddress')
|
|
21
|
+
assert(typeof to === 'string' && to, 'expected string toAddress')
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
chainId,
|
|
25
|
+
from,
|
|
26
|
+
to,
|
|
27
|
+
gas: `0x${parsed.gasLimit.toString(16)}`,
|
|
28
|
+
gasPrice: `0x${BigInt(gasPriceUnit.toBaseString()).toString(16)}`,
|
|
29
|
+
value: '0x0',
|
|
30
|
+
data: `0x${parsed.data?.toString('hex')}`,
|
|
31
|
+
nonce: `0x${parsed.nonce.toString(16)}`,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
approveTxRequest: build(unsignedTxApprove),
|
|
37
|
+
delegateTxRequest: build(unsignedTxDelegate),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function maticDelegateSimulateTransactions({
|
|
42
|
+
asset,
|
|
43
|
+
unsignedTxApprove,
|
|
44
|
+
unsignedTxDelegate,
|
|
45
|
+
senderAddress,
|
|
46
|
+
revertOnSimulationError,
|
|
47
|
+
}) {
|
|
48
|
+
const { approveTxRequest, delegateTxRequest } = createMaticStakeSimRequestsFromUnsigned({
|
|
49
|
+
asset: asset.baseAsset,
|
|
50
|
+
unsignedTxApprove,
|
|
51
|
+
unsignedTxDelegate,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Simulate transactions
|
|
55
|
+
const simulationTxData = {
|
|
56
|
+
baseAssetName: asset.baseAsset.name,
|
|
57
|
+
origin: 'exodus-staking',
|
|
58
|
+
senderAddress,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const simulationResponse = await asset.baseAsset.api.web3.simulateTransactions({
|
|
62
|
+
...simulationTxData,
|
|
63
|
+
transactions: [approveTxRequest, delegateTxRequest],
|
|
64
|
+
})
|
|
65
|
+
// These checks are only for simulation issues, not validation of the transactions
|
|
66
|
+
if (simulationResponse?.warnings?.length || simulationResponse?.metadata?.humanReadableError) {
|
|
67
|
+
if (revertOnSimulationError) {
|
|
68
|
+
const err = new Error('StakingMaticSimulationError')
|
|
69
|
+
err.message = simulationResponse?.metadata?.humanReadableError
|
|
70
|
+
err.reason = `warnings: ${simulationResponse.warnings.join(', ')}`
|
|
71
|
+
err.hint = 'delegate-matic-simulation-error'
|
|
72
|
+
throw err
|
|
73
|
+
} else {
|
|
74
|
+
console.warn(
|
|
75
|
+
'Simulation for staking matic returned issues',
|
|
76
|
+
simulationResponse?.metadata?.humanReadableError
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// For validation of the transactions, we need to check if the simulation produced the expected effects
|
|
82
|
+
// hasApprove, hasSend, hasReceive entries must be present
|
|
83
|
+
const { balanceChanges = Object.create(null) } = simulationResponse || Object.create(null)
|
|
84
|
+
const asArray = (v) => (Array.isArray(v) ? v : [])
|
|
85
|
+
const hasApprove = asArray(balanceChanges.willApprove).length > 0
|
|
86
|
+
const hasSend = asArray(balanceChanges.willSend).length > 0
|
|
87
|
+
const hasReceive = asArray(balanceChanges.willReceive).length > 0
|
|
88
|
+
|
|
89
|
+
if (!(hasApprove && hasSend && hasReceive)) {
|
|
90
|
+
if (revertOnSimulationError) {
|
|
91
|
+
const missing = []
|
|
92
|
+
if (!hasApprove) missing.push('willApprove')
|
|
93
|
+
if (!hasSend) missing.push('willSend')
|
|
94
|
+
if (!hasReceive) missing.push('willReceive')
|
|
95
|
+
const err = new Error('StakingMaticSimulationResultsError')
|
|
96
|
+
err.message = 'Simulation did not produce expected effects'
|
|
97
|
+
err.reason = `missing balanceChanges: ${missing.join(', ')}`
|
|
98
|
+
err.hint = 'delegate-matic-simulation-results-error'
|
|
99
|
+
throw err
|
|
100
|
+
} else {
|
|
101
|
+
console.warn(
|
|
102
|
+
'Simulation for staking matic returned issues',
|
|
103
|
+
simulationResponse?.metadata?.humanReadableError
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -1,158 +1,299 @@
|
|
|
1
|
+
import { memoize } from '@exodus/basic-utils'
|
|
1
2
|
import { isNumberUnit } from '@exodus/currency'
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import {
|
|
4
|
+
estimateGasLimit,
|
|
5
|
+
getAggregateTransactionPricing,
|
|
6
|
+
getOptimisticTxLogEffects,
|
|
7
|
+
scaleGasLimitEstimate,
|
|
8
|
+
stakingProviderClientFactory,
|
|
9
|
+
} from '@exodus/ethereum-api'
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
import { maticDelegateSimulateTransactions } from './matic-staking-utils.js'
|
|
12
|
+
|
|
13
|
+
export function stakingServiceFactory({ assetClientInterface, server: _server, stakingServer }) {
|
|
6
14
|
const stakingProvider = stakingProviderClientFactory()
|
|
15
|
+
const assetName = 'ethereum'
|
|
7
16
|
|
|
8
17
|
function amountToCurrency({ asset, amount }) {
|
|
9
18
|
return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
|
|
10
19
|
}
|
|
11
20
|
|
|
12
|
-
|
|
21
|
+
const getStakeAssets = memoize(async () => {
|
|
13
22
|
const { polygon: asset, ethereum: feeAsset } = await assetClientInterface.getAssetsForNetwork({
|
|
14
|
-
baseAssetName:
|
|
23
|
+
baseAssetName: assetName, // Polygon token lives in ETH network
|
|
15
24
|
})
|
|
16
25
|
return { asset, feeAsset }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Helper function which selects the correct `asset` to use for when
|
|
29
|
+
// determining `feeData` for MATIC staking.
|
|
30
|
+
const resolveFeeData = async ({ feeData }) => {
|
|
31
|
+
const { feeAsset } = await getStakeAssets()
|
|
32
|
+
if (feeData) return feeData
|
|
33
|
+
|
|
34
|
+
console.warn(
|
|
35
|
+
'The evm staking service was not explicitly passed `feeData`. This can result in transaction nondeterminism.'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return assetClientInterface.getFeeData({ assetName: feeAsset.name })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getLatestFeeData = async () => {
|
|
42
|
+
const { feeAsset } = await getStakeAssets()
|
|
43
|
+
return assetClientInterface.getFeeData({ assetName: feeAsset.name })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const resolveOptionalFeeData = async ({ feeData }) => {
|
|
47
|
+
// If the caller provides truthy `feeData`, we can continue
|
|
48
|
+
// as normal.
|
|
49
|
+
if (feeData) return feeData
|
|
50
|
+
|
|
51
|
+
// If the caller specifically omits `feeData`, then we
|
|
52
|
+
// provide a backup without warning. This is useful for
|
|
53
|
+
// calls with no expectations on the caller to provide
|
|
54
|
+
// `feeData`, i.e. one-shot transactions, or transactions
|
|
55
|
+
// which do not render a fee estimation.
|
|
56
|
+
return getLatestFeeData()
|
|
17
57
|
}
|
|
18
58
|
|
|
19
|
-
async
|
|
20
|
-
const { asset } = await getStakeAssets()
|
|
59
|
+
const getDelegatorAddress = async ({ walletAccount }) => {
|
|
21
60
|
const address = await assetClientInterface.getReceiveAddress({
|
|
22
|
-
assetName
|
|
61
|
+
assetName,
|
|
23
62
|
walletAccount,
|
|
24
63
|
})
|
|
25
|
-
|
|
64
|
+
return address.toLowerCase()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function approveDelegateAmount({ walletAccount, amount, feeData } = {}) {
|
|
68
|
+
feeData = await resolveOptionalFeeData({ feeData })
|
|
69
|
+
|
|
70
|
+
const [delegatorAddress, { asset }] = await Promise.all([
|
|
71
|
+
getDelegatorAddress({ walletAccount }),
|
|
72
|
+
getStakeAssets(),
|
|
73
|
+
])
|
|
26
74
|
|
|
27
75
|
amount = amountToCurrency({ asset, amount })
|
|
28
76
|
|
|
29
77
|
const txApproveData = await stakingServer.approveStakeManager(amount)
|
|
30
|
-
const { gasPrice, gasLimit,
|
|
31
|
-
delegatorAddress,
|
|
32
|
-
|
|
33
|
-
txApproveData
|
|
34
|
-
|
|
78
|
+
const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
|
|
79
|
+
from: delegatorAddress,
|
|
80
|
+
to: stakingServer.polygonContract.address,
|
|
81
|
+
txInput: txApproveData,
|
|
82
|
+
feeData,
|
|
83
|
+
})
|
|
35
84
|
|
|
36
85
|
return prepareAndSendTx({
|
|
37
86
|
walletAccount,
|
|
38
87
|
waitForConfirmation: true,
|
|
39
|
-
to:
|
|
88
|
+
to: stakingServer.polygonContract.address,
|
|
40
89
|
txData: txApproveData,
|
|
41
90
|
gasPrice,
|
|
42
91
|
gasLimit,
|
|
43
|
-
|
|
92
|
+
tipGasPrice,
|
|
93
|
+
feeData,
|
|
44
94
|
})
|
|
45
95
|
}
|
|
46
96
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Delegate MATIC tokens for staking.
|
|
99
|
+
* @param {walletAccount} params.walletAccount - The walletAccount for the delegator (required).
|
|
100
|
+
* @param {NumberUnit} params.amount - The amount of MATIC to delegate (required).
|
|
101
|
+
* @param {FeeData} params.feeData - Optional feeData to use for the transaction (optional, if not provided will be obtained from the asset client interface).
|
|
102
|
+
* @param {Boolean} params.revertOnSimulationError - Optional boolean to revert on simulation error (optional, default is true). Allows us to quickly react to simulator issues.
|
|
103
|
+
*/
|
|
104
|
+
async function delegate({ walletAccount, amount, feeData, revertOnSimulationError = true } = {}) {
|
|
105
|
+
const [delegatorAddress, { asset, feeAsset }, resolvedFeeData] = await Promise.all([
|
|
106
|
+
getDelegatorAddress({ walletAccount }),
|
|
107
|
+
getStakeAssets(),
|
|
108
|
+
resolveFeeData({ feeData }),
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
feeData = resolvedFeeData
|
|
112
|
+
|
|
113
|
+
amount = amountToCurrency({ asset, amount })
|
|
114
|
+
|
|
115
|
+
const baseNonce = await feeAsset.getNonce({
|
|
116
|
+
asset: feeAsset,
|
|
117
|
+
fromAddress: delegatorAddress.toString(),
|
|
51
118
|
walletAccount,
|
|
52
119
|
})
|
|
53
|
-
const delegatorAddress = address.toLowerCase()
|
|
54
120
|
|
|
55
|
-
|
|
121
|
+
const [txApproveData, txDelegateData] = await Promise.all([
|
|
122
|
+
stakingServer.approveStakeManager(amount),
|
|
123
|
+
stakingServer.delegate({ amount }),
|
|
124
|
+
])
|
|
56
125
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// txData: txApproveData,
|
|
68
|
-
// gasPrice,
|
|
69
|
-
// gasLimit,
|
|
70
|
-
// fee,
|
|
71
|
-
// })
|
|
72
|
-
|
|
73
|
-
const txDelegateData = await stakingServer.delegate({ amount })
|
|
74
|
-
const { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
75
|
-
delegatorAddress,
|
|
76
|
-
asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
|
|
77
|
-
txDelegateData
|
|
78
|
-
)
|
|
126
|
+
const {
|
|
127
|
+
gasPrice,
|
|
128
|
+
gasLimit: approveGasLimit,
|
|
129
|
+
tipGasPrice,
|
|
130
|
+
} = await estimateTxFee({
|
|
131
|
+
from: delegatorAddress,
|
|
132
|
+
to: stakingServer.polygonContract.address,
|
|
133
|
+
txInput: txApproveData,
|
|
134
|
+
feeData,
|
|
135
|
+
})
|
|
79
136
|
|
|
80
|
-
|
|
137
|
+
// Estimate based on the average gas usage for the delegate transaction. 250000 is the average.
|
|
138
|
+
const { gasLimit: estimatedDelegateGasLimit } = await estimateDelegateTxFee({
|
|
139
|
+
feeData,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const createTxBaseArgs = {
|
|
143
|
+
asset: feeAsset,
|
|
81
144
|
walletAccount,
|
|
82
|
-
|
|
83
|
-
|
|
145
|
+
fromAddress: delegatorAddress.toString(),
|
|
146
|
+
amount: feeAsset.currency.ZERO,
|
|
147
|
+
tipGasPrice,
|
|
84
148
|
gasPrice,
|
|
85
|
-
|
|
86
|
-
|
|
149
|
+
}
|
|
150
|
+
const [unsignedTxApprove, unsignedTxDelegate] = await Promise.all([
|
|
151
|
+
feeAsset.api.createTx({
|
|
152
|
+
...createTxBaseArgs,
|
|
153
|
+
toAddress: stakingServer.polygonContract.address,
|
|
154
|
+
txInput: txApproveData,
|
|
155
|
+
gasLimit: approveGasLimit,
|
|
156
|
+
nonce: baseNonce,
|
|
157
|
+
}),
|
|
158
|
+
feeAsset.api.createTx({
|
|
159
|
+
...createTxBaseArgs,
|
|
160
|
+
toAddress: stakingServer.validatorShareContract.address,
|
|
161
|
+
txInput: txDelegateData,
|
|
162
|
+
gasLimit: estimatedDelegateGasLimit,
|
|
163
|
+
nonce: baseNonce + 1,
|
|
164
|
+
}),
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
await maticDelegateSimulateTransactions({
|
|
168
|
+
asset,
|
|
169
|
+
unsignedTxApprove,
|
|
170
|
+
unsignedTxDelegate,
|
|
171
|
+
senderAddress: delegatorAddress.toString(),
|
|
172
|
+
revertOnSimulationError,
|
|
87
173
|
})
|
|
88
174
|
|
|
175
|
+
// Sign transactions
|
|
176
|
+
const [approveSigned, delegateSigned] = await Promise.all([
|
|
177
|
+
assetClientInterface.signTransaction({
|
|
178
|
+
assetName: feeAsset.name,
|
|
179
|
+
unsignedTx: unsignedTxApprove.unsignedTx,
|
|
180
|
+
walletAccount,
|
|
181
|
+
}),
|
|
182
|
+
assetClientInterface.signTransaction({
|
|
183
|
+
assetName: feeAsset.name,
|
|
184
|
+
unsignedTx: unsignedTxDelegate.unsignedTx,
|
|
185
|
+
walletAccount,
|
|
186
|
+
}),
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
// Pre-compute txIDs
|
|
190
|
+
const approveTxId = `0x${approveSigned.txId.toString('hex')}`
|
|
191
|
+
const delegateTxId = `0x${delegateSigned.txId.toString('hex')}`
|
|
192
|
+
|
|
193
|
+
const bundleResponse = await feeAsset.broadcastPrivateBundle({
|
|
194
|
+
txs: [approveSigned, delegateSigned].map(({ rawTx }) => rawTx),
|
|
195
|
+
})
|
|
196
|
+
const bundleHash = bundleResponse?.bundleHash
|
|
197
|
+
|
|
198
|
+
const { optimisticTxLogEffects: approveOptimisticTxLogEffects } =
|
|
199
|
+
await getOptimisticTxLogEffects({
|
|
200
|
+
asset: feeAsset,
|
|
201
|
+
assetClientInterface,
|
|
202
|
+
fromAddress: delegatorAddress.toString(),
|
|
203
|
+
txId: approveTxId,
|
|
204
|
+
unsignedTx: unsignedTxApprove.unsignedTx,
|
|
205
|
+
walletAccount,
|
|
206
|
+
bundleId: bundleHash ?? undefined,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const { optimisticTxLogEffects: delegateOptimisticTxLogEffects } =
|
|
210
|
+
await getOptimisticTxLogEffects({
|
|
211
|
+
asset: feeAsset,
|
|
212
|
+
assetClientInterface,
|
|
213
|
+
fromAddress: delegatorAddress.toString(),
|
|
214
|
+
txId: delegateTxId,
|
|
215
|
+
unsignedTx: unsignedTxDelegate.unsignedTx,
|
|
216
|
+
walletAccount,
|
|
217
|
+
bundleId: bundleHash ?? undefined,
|
|
218
|
+
})
|
|
219
|
+
// Combine optimistic effects from both transactions
|
|
220
|
+
const tokenOptimisticEffects = [
|
|
221
|
+
...approveOptimisticTxLogEffects,
|
|
222
|
+
...delegateOptimisticTxLogEffects,
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
for (const optimisticEffect of tokenOptimisticEffects) {
|
|
226
|
+
await assetClientInterface.updateTxLogAndNotify(optimisticEffect)
|
|
227
|
+
}
|
|
228
|
+
|
|
89
229
|
await stakingProvider.notifyStaking({
|
|
90
|
-
txId,
|
|
230
|
+
txId: delegateTxId,
|
|
91
231
|
asset: asset.name,
|
|
92
232
|
delegator: delegatorAddress,
|
|
93
233
|
amount: amount.toBaseString(),
|
|
94
234
|
})
|
|
95
|
-
|
|
96
|
-
return txId
|
|
235
|
+
return delegateTxId
|
|
97
236
|
}
|
|
98
237
|
|
|
99
|
-
async function undelegate({ walletAccount, amount } = {}) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
walletAccount,
|
|
104
|
-
|
|
105
|
-
|
|
238
|
+
async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = false } = {}) {
|
|
239
|
+
feeData = await resolveOptionalFeeData({ feeData })
|
|
240
|
+
|
|
241
|
+
const [delegatorAddress, { asset }] = await Promise.all([
|
|
242
|
+
getDelegatorAddress({ walletAccount }),
|
|
243
|
+
getStakeAssets(),
|
|
244
|
+
])
|
|
106
245
|
|
|
107
246
|
amount = amountToCurrency({ asset, amount })
|
|
108
247
|
|
|
109
248
|
const txUndelegateData = await stakingServer.undelegate({ amount })
|
|
110
|
-
const { gasPrice, gasLimit,
|
|
111
|
-
delegatorAddress.toLowerCase(),
|
|
112
|
-
|
|
113
|
-
txUndelegateData
|
|
114
|
-
|
|
249
|
+
const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
|
|
250
|
+
from: delegatorAddress.toLowerCase(),
|
|
251
|
+
to: stakingServer.validatorShareContract.address,
|
|
252
|
+
txInput: txUndelegateData,
|
|
253
|
+
feeData,
|
|
254
|
+
})
|
|
115
255
|
return prepareAndSendTx({
|
|
116
256
|
walletAccount,
|
|
117
|
-
to:
|
|
257
|
+
to: stakingServer.validatorShareContract.address,
|
|
118
258
|
txData: txUndelegateData,
|
|
119
259
|
gasPrice,
|
|
120
260
|
gasLimit,
|
|
121
|
-
|
|
261
|
+
tipGasPrice,
|
|
262
|
+
waitForConfirmation,
|
|
263
|
+
feeData,
|
|
122
264
|
})
|
|
123
265
|
}
|
|
124
266
|
|
|
125
|
-
async function claimRewards({ walletAccount } = {}) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
walletAccount,
|
|
130
|
-
})
|
|
131
|
-
const delegatorAddress = address.toLowerCase()
|
|
267
|
+
async function claimRewards({ walletAccount, feeData } = {}) {
|
|
268
|
+
feeData = await resolveOptionalFeeData({ feeData })
|
|
269
|
+
|
|
270
|
+
const delegatorAddress = await getDelegatorAddress({ walletAccount })
|
|
132
271
|
|
|
133
272
|
const txWithdrawRewardsData = await stakingServer.withdrawRewards()
|
|
134
|
-
const { gasPrice, gasLimit,
|
|
135
|
-
delegatorAddress,
|
|
136
|
-
|
|
137
|
-
txWithdrawRewardsData
|
|
138
|
-
|
|
273
|
+
const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
|
|
274
|
+
from: delegatorAddress,
|
|
275
|
+
to: stakingServer.validatorShareContract.address,
|
|
276
|
+
txInput: txWithdrawRewardsData,
|
|
277
|
+
feeData,
|
|
278
|
+
})
|
|
139
279
|
return prepareAndSendTx({
|
|
140
280
|
walletAccount,
|
|
141
|
-
to:
|
|
281
|
+
to: stakingServer.validatorShareContract.address,
|
|
142
282
|
txData: txWithdrawRewardsData,
|
|
143
283
|
gasPrice,
|
|
144
284
|
gasLimit,
|
|
145
|
-
|
|
285
|
+
tipGasPrice,
|
|
286
|
+
feeData,
|
|
146
287
|
})
|
|
147
288
|
}
|
|
148
289
|
|
|
149
|
-
async function claimUndelegatedBalance({ walletAccount, unbondNonce } = {}) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
walletAccount,
|
|
154
|
-
|
|
155
|
-
|
|
290
|
+
async function claimUndelegatedBalance({ walletAccount, unbondNonce, feeData } = {}) {
|
|
291
|
+
feeData = await resolveOptionalFeeData({ feeData })
|
|
292
|
+
|
|
293
|
+
const [delegatorAddress, { asset }] = await Promise.all([
|
|
294
|
+
getDelegatorAddress({ walletAccount }),
|
|
295
|
+
getStakeAssets(),
|
|
296
|
+
])
|
|
156
297
|
|
|
157
298
|
const { currency } = asset
|
|
158
299
|
const unstakedClaimInfo = await fetchUnstakedClaimInfo({
|
|
@@ -168,18 +309,20 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
|
|
|
168
309
|
})
|
|
169
310
|
|
|
170
311
|
const txClaimUndelegatedData = await stakingServer.claimUndelegatedBalance({ unbondNonce })
|
|
171
|
-
const { gasPrice, gasLimit,
|
|
172
|
-
delegatorAddress,
|
|
173
|
-
|
|
174
|
-
txClaimUndelegatedData
|
|
175
|
-
|
|
312
|
+
const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
|
|
313
|
+
from: delegatorAddress,
|
|
314
|
+
to: stakingServer.validatorShareContract.address,
|
|
315
|
+
txInput: txClaimUndelegatedData,
|
|
316
|
+
feeData,
|
|
317
|
+
})
|
|
176
318
|
const txId = await prepareAndSendTx({
|
|
177
319
|
walletAccount,
|
|
178
|
-
to:
|
|
320
|
+
to: stakingServer.validatorShareContract.address,
|
|
179
321
|
txData: txClaimUndelegatedData,
|
|
180
322
|
gasPrice,
|
|
181
323
|
gasLimit,
|
|
182
|
-
|
|
324
|
+
tipGasPrice,
|
|
325
|
+
feeData,
|
|
183
326
|
})
|
|
184
327
|
|
|
185
328
|
await stakingProvider.notifyUnstaking({
|
|
@@ -192,19 +335,29 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
|
|
|
192
335
|
return txId
|
|
193
336
|
}
|
|
194
337
|
|
|
195
|
-
async function estimateDelegateOperation({
|
|
196
|
-
|
|
338
|
+
async function estimateDelegateOperation({
|
|
339
|
+
walletAccount,
|
|
340
|
+
operation,
|
|
341
|
+
args,
|
|
342
|
+
// NOTE: When estimating transactions, ideally we'd expect the `feeData`
|
|
343
|
+
// that we intend to send the transaction using. If this is not
|
|
344
|
+
// defined, we'll fallback to a default (with a warning).
|
|
345
|
+
feeData,
|
|
346
|
+
}) {
|
|
347
|
+
// HACK: For delegation transactions, we must fall back to the
|
|
348
|
+
// custom implementation, since we can't currently estimate
|
|
349
|
+
// the transaction due to a dependence upon approvals.
|
|
350
|
+
if (operation === 'delegate') return estimateDelegateTxFee({ feeData })
|
|
351
|
+
|
|
352
|
+
const [delegatorAddress, { asset }] = await Promise.all([
|
|
353
|
+
getDelegatorAddress({ walletAccount }),
|
|
354
|
+
getStakeAssets(),
|
|
355
|
+
])
|
|
197
356
|
|
|
198
|
-
|
|
199
|
-
return
|
|
200
|
-
}
|
|
357
|
+
feeData = await resolveFeeData({ feeData })
|
|
201
358
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
assetName: asset.name,
|
|
205
|
-
walletAccount,
|
|
206
|
-
})
|
|
207
|
-
const delegatorAddress = address.toLowerCase()
|
|
359
|
+
const delegateOperation = stakingServer[operation]
|
|
360
|
+
if (!delegateOperation) return
|
|
208
361
|
|
|
209
362
|
const { amount } = args
|
|
210
363
|
if (amount) {
|
|
@@ -212,11 +365,12 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
|
|
|
212
365
|
}
|
|
213
366
|
|
|
214
367
|
const operationTxData = await delegateOperation({ ...args, walletAccount })
|
|
215
|
-
const { fee } = await estimateTxFee(
|
|
216
|
-
delegatorAddress,
|
|
217
|
-
|
|
218
|
-
operationTxData
|
|
219
|
-
|
|
368
|
+
const { fee } = await estimateTxFee({
|
|
369
|
+
from: delegatorAddress,
|
|
370
|
+
to: stakingServer.validatorShareContract.address,
|
|
371
|
+
txInput: operationTxData,
|
|
372
|
+
feeData,
|
|
373
|
+
})
|
|
220
374
|
|
|
221
375
|
return fee
|
|
222
376
|
}
|
|
@@ -231,21 +385,25 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
|
|
|
231
385
|
* This is just for displaying purposes and it's just an aproximation of the delegate gas cost,
|
|
232
386
|
* NOT the real fee cost
|
|
233
387
|
*/
|
|
234
|
-
async function estimateDelegateTxFee() {
|
|
388
|
+
async function estimateDelegateTxFee({ feeData } = Object.create(null)) {
|
|
235
389
|
// approx gas limits
|
|
236
390
|
const { feeAsset } = await getStakeAssets()
|
|
237
|
-
const erc20ApproveGas = 4900
|
|
238
|
-
const delegateGas = 240_000
|
|
239
|
-
const gasPrice = parseInt(await server.gasPrice(), 16)
|
|
240
|
-
const extraPercentage = 20
|
|
241
|
-
|
|
242
|
-
const gasLimit = erc20ApproveGas + delegateGas
|
|
243
|
-
const gasLimitWithBuffer = new BN(gasLimit)
|
|
244
|
-
.imuln(100 + extraPercentage)
|
|
245
|
-
.idivn(100)
|
|
246
|
-
.toString()
|
|
247
391
|
|
|
248
|
-
|
|
392
|
+
feeData = await resolveFeeData({ feeData })
|
|
393
|
+
|
|
394
|
+
// TODO: update estimation to use a mock source address for
|
|
395
|
+
// deposits so we can simulate the necessary approvals,
|
|
396
|
+
// we shouldn't maintain constants like these
|
|
397
|
+
const erc20ApproveGas = 80_000
|
|
398
|
+
const delegateGas = 250_000
|
|
399
|
+
|
|
400
|
+
const { gasPrice } = getAggregateTransactionPricing({ baseAsset: feeAsset, feeData })
|
|
401
|
+
|
|
402
|
+
const gasLimitWithBuffer = scaleGasLimitEstimate({
|
|
403
|
+
estimatedGasLimit: BigInt(erc20ApproveGas + delegateGas),
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const fee = BigInt(gasLimitWithBuffer) * BigInt(gasPrice.toBaseNumber())
|
|
249
407
|
|
|
250
408
|
return {
|
|
251
409
|
gasLimit: gasLimitWithBuffer,
|
|
@@ -254,31 +412,22 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
|
|
|
254
412
|
}
|
|
255
413
|
}
|
|
256
414
|
|
|
257
|
-
async function estimateTxFee(from, to, txInput,
|
|
415
|
+
async function estimateTxFee({ from, to, txInput, feeData }) {
|
|
258
416
|
const { feeAsset } = await getStakeAssets()
|
|
259
417
|
|
|
260
418
|
const amount = feeAsset.currency.ZERO
|
|
419
|
+
|
|
261
420
|
const gasLimit = await estimateGasLimit({
|
|
262
421
|
asset: feeAsset,
|
|
263
422
|
fromAddress: from,
|
|
264
423
|
toAddress: to,
|
|
265
|
-
amount,
|
|
424
|
+
amount, // staking contracts does not require ETH amount to interact with
|
|
266
425
|
data: txInput,
|
|
267
|
-
gasPrice,
|
|
426
|
+
gasPrice: '0x0', // DISABLE_BALANCE_CHECKS equivalent
|
|
268
427
|
})
|
|
269
428
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
gasPrice = parseInt(gasPrice, 16)
|
|
275
|
-
const fee = new BN(gasPrice).mul(new BN(gasLimit))
|
|
276
|
-
|
|
277
|
-
return {
|
|
278
|
-
gasLimit,
|
|
279
|
-
gasPrice: feeAsset.currency.baseUnit(gasPrice),
|
|
280
|
-
fee: feeAsset.currency.baseUnit(fee.toString()),
|
|
281
|
-
}
|
|
429
|
+
const fee = feeAsset.api.getFee({ asset: feeAsset, feeData, gasLimit, amount })
|
|
430
|
+
return { ...fee, gasLimit }
|
|
282
431
|
}
|
|
283
432
|
|
|
284
433
|
async function prepareAndSendTx({
|
|
@@ -287,30 +436,25 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
|
|
|
287
436
|
txData: txInput,
|
|
288
437
|
gasPrice,
|
|
289
438
|
gasLimit,
|
|
290
|
-
|
|
439
|
+
tipGasPrice,
|
|
291
440
|
waitForConfirmation = false,
|
|
441
|
+
feeData,
|
|
292
442
|
} = {}) {
|
|
293
|
-
const {
|
|
294
|
-
|
|
443
|
+
const { feeAsset } = await getStakeAssets()
|
|
295
444
|
const sendTxArgs = {
|
|
296
445
|
asset: feeAsset,
|
|
297
446
|
walletAccount,
|
|
298
447
|
address: to,
|
|
299
448
|
amount: feeAsset.currency.ZERO,
|
|
300
|
-
// used in desktop,
|
|
301
|
-
// remove once, txSend is unified
|
|
302
|
-
receiver: {
|
|
303
|
-
address: to,
|
|
304
|
-
amount: feeAsset.currency.ZERO,
|
|
305
|
-
},
|
|
306
449
|
txInput,
|
|
307
450
|
gasPrice,
|
|
308
451
|
gasLimit,
|
|
309
|
-
|
|
452
|
+
tipGasPrice,
|
|
310
453
|
waitForConfirmation,
|
|
454
|
+
feeData,
|
|
311
455
|
}
|
|
312
456
|
|
|
313
|
-
const { txId } = await
|
|
457
|
+
const { txId } = await feeAsset.api.sendTx(sendTxArgs)
|
|
314
458
|
|
|
315
459
|
return txId
|
|
316
460
|
}
|
|
@@ -318,12 +462,18 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
|
|
|
318
462
|
const getStakingInfo = getPolygonStakingInfo({ assetClientInterface, stakingServer })
|
|
319
463
|
|
|
320
464
|
return {
|
|
465
|
+
// Unified API (used by stakingApiFactory when exposed on asset.api.staking)
|
|
466
|
+
approveStake: approveDelegateAmount,
|
|
467
|
+
stake: delegate,
|
|
468
|
+
unstake: undelegate,
|
|
469
|
+
claimUnstaked: claimUndelegatedBalance,
|
|
470
|
+
claimRewards,
|
|
471
|
+
getStakingInfo,
|
|
472
|
+
// Legacy names (for direct service usage)
|
|
321
473
|
approveDelegateAmount,
|
|
322
474
|
delegate,
|
|
323
475
|
undelegate,
|
|
324
|
-
claimRewards,
|
|
325
476
|
claimUndelegatedBalance,
|
|
326
|
-
getStakingInfo,
|
|
327
477
|
estimateDelegateTxFee,
|
|
328
478
|
estimateDelegateOperation,
|
|
329
479
|
}
|
|
@@ -406,18 +556,20 @@ async function getUnstakedUnclaimedInfo({
|
|
|
406
556
|
}
|
|
407
557
|
|
|
408
558
|
async function fetchRewardsInfo({ stakingServer, delegator, currency }) {
|
|
409
|
-
const [minRewardsToWithdraw, rewardsBalance] = await Promise.all([
|
|
559
|
+
const [minRewardsToWithdraw, lastRewards, rewardsBalance] = await Promise.all([
|
|
410
560
|
stakingServer.getMinRewardsToWithdraw(),
|
|
411
561
|
stakingServer.getLiquidRewards(delegator),
|
|
562
|
+
stakingServer.getTotalRewards(delegator),
|
|
412
563
|
])
|
|
413
|
-
const withdrawable =
|
|
414
|
-
?
|
|
564
|
+
const withdrawable = lastRewards.sub(minRewardsToWithdraw).gte(currency.ZERO)
|
|
565
|
+
? lastRewards
|
|
415
566
|
: currency.ZERO
|
|
416
567
|
|
|
417
568
|
return {
|
|
418
|
-
rewardsBalance,
|
|
569
|
+
rewardsBalance: rewardsBalance.add(lastRewards), // all time accrued rewards
|
|
570
|
+
liquidRewards: lastRewards, // current pending rewards (on-chain)
|
|
419
571
|
minRewardsToWithdraw,
|
|
420
|
-
withdrawable,
|
|
572
|
+
withdrawable, // unclaimed rewards
|
|
421
573
|
}
|
|
422
574
|
}
|
|
423
575
|
|
|
@@ -430,7 +582,7 @@ export function getPolygonStakingInfo({ assetClientInterface, stakingServer }) {
|
|
|
430
582
|
const delegator = address.toLowerCase()
|
|
431
583
|
const [
|
|
432
584
|
delegatedBalance,
|
|
433
|
-
{ rewardsBalance, minRewardsToWithdraw, withdrawable },
|
|
585
|
+
{ rewardsBalance, liquidRewards, minRewardsToWithdraw, withdrawable },
|
|
434
586
|
{ unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate },
|
|
435
587
|
] = await Promise.all([
|
|
436
588
|
stakingServer.getTotalStake(delegator),
|
|
@@ -452,6 +604,7 @@ export function getPolygonStakingInfo({ assetClientInterface, stakingServer }) {
|
|
|
452
604
|
|
|
453
605
|
return {
|
|
454
606
|
rewardsBalance,
|
|
607
|
+
liquidRewards,
|
|
455
608
|
withdrawable,
|
|
456
609
|
unbondNonce,
|
|
457
610
|
isDelegating,
|
package/src/staking.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ethereumStakingDeps } from '@exodus/ethereum-api'
|
|
1
2
|
import { ethStakeAccountState } from '@exodus/ethereum-lib'
|
|
2
3
|
import { asset } from '@exodus/ethereum-meta'
|
|
3
4
|
|
|
@@ -9,5 +10,6 @@ export const stakingConfiguration = {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const stakingDependencies = {
|
|
13
|
+
ethereum: ethereumStakingDeps,
|
|
12
14
|
polygon: polygonStakingDeps,
|
|
13
15
|
}
|