@exodus/ethereum-api 8.17.0 → 8.18.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 +9 -0
- package/package.json +3 -3
- package/src/staking/api/index.js +34 -0
- package/src/staking/ethereum/service.js +125 -81
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
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.18.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.17.0...@exodus/ethereum-api@8.18.0) (2024-09-06)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* evm staking api factory ([#2473](https://github.com/ExodusMovement/assets/issues/2473)) ([3159264](https://github.com/ExodusMovement/assets/commit/315926414cfa99cb51fde2bcb44bbea478275d79))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## [8.17.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.16.0...@exodus/ethereum-api@8.17.0) (2024-08-30)
|
|
7
16
|
|
|
8
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.18.0",
|
|
4
4
|
"description": "Ethereum Api",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "run -T exodus-test --jest",
|
|
20
|
-
"lint": "run -T
|
|
20
|
+
"lint": "run -T eslintc .",
|
|
21
21
|
"lint:fix": "yarn lint --fix"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"type": "git",
|
|
66
66
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "b03f9102b0401e40f0484c2195061a907eae9b47"
|
|
69
69
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { pick } from '@exodus/basic-utils'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
export const stakingApiFactory = ({
|
|
5
|
+
asset,
|
|
6
|
+
assetClientInterface,
|
|
7
|
+
server, // coin node server
|
|
8
|
+
stakingConfiguration,
|
|
9
|
+
stakingDependencies,
|
|
10
|
+
}) => {
|
|
11
|
+
assert(asset, '"asset" is required')
|
|
12
|
+
assert(assetClientInterface, '"assetClientInterface" is required')
|
|
13
|
+
assert(server, '"server" is required')
|
|
14
|
+
assert(stakingConfiguration, '"stakingConfiguration" is required')
|
|
15
|
+
assert(stakingDependencies, '"stakingDependencies" is required')
|
|
16
|
+
|
|
17
|
+
const { contracts, minAmount } = stakingConfiguration
|
|
18
|
+
const { stakingServerFactory, stakingServiceFactory } = stakingDependencies
|
|
19
|
+
|
|
20
|
+
const stakingServer = stakingServerFactory({ asset, server, minAmount, contracts })
|
|
21
|
+
|
|
22
|
+
const stakingService = stakingServiceFactory({
|
|
23
|
+
assetClientInterface,
|
|
24
|
+
server,
|
|
25
|
+
stakingServer,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
isStaking: async ({ isDelegating }) => isDelegating,
|
|
30
|
+
isUnstaking: async ({ isUndelegateInProgress }) => isUndelegateInProgress,
|
|
31
|
+
isUnstaked: async ({ canClaimUndelegateBalance }) => canClaimUndelegateBalance,
|
|
32
|
+
...pick(stakingService, ['approveStake', 'stake', 'unstake', 'claimUnstaked', 'claimRewards']),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -15,12 +15,13 @@ export function createEthereumStakingService({
|
|
|
15
15
|
}) {
|
|
16
16
|
const staking = new EthereumStaking(asset)
|
|
17
17
|
const stakingProvider = stakingProviderClientFactory()
|
|
18
|
+
const minAmount = staking.minAmount
|
|
18
19
|
|
|
19
20
|
function amountToCurrency({ asset, amount }) {
|
|
20
21
|
return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
async function delegate({ walletAccount, amount } =
|
|
24
|
+
async function delegate({ walletAccount, amount } = Object.create(null)) {
|
|
24
25
|
const address = await assetClientInterface.getReceiveAddress({
|
|
25
26
|
assetName: asset.name,
|
|
26
27
|
walletAccount,
|
|
@@ -62,95 +63,149 @@ export function createEthereumStakingService({
|
|
|
62
63
|
return txId
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
async function
|
|
66
|
+
async function getUndelegatePendingData({
|
|
67
|
+
delegatorAddress,
|
|
68
|
+
resquestedAmount,
|
|
69
|
+
pendingAmount,
|
|
70
|
+
minAmount,
|
|
71
|
+
}) {
|
|
72
|
+
const leftOver = pendingAmount.sub(resquestedAmount)
|
|
73
|
+
|
|
74
|
+
if (leftOver.isPositive && leftOver.lt(minAmount)) {
|
|
75
|
+
throw new Error(`Pending balance less than min stake amount ${minAmount}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const inactiveAmountToUnstake = pendingAmount.lte(resquestedAmount)
|
|
79
|
+
? pendingAmount
|
|
80
|
+
: resquestedAmount
|
|
81
|
+
const { to, data } = await staking.unstakePending({
|
|
82
|
+
address: delegatorAddress,
|
|
83
|
+
amount: inactiveAmountToUnstake,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
let feeData
|
|
87
|
+
try {
|
|
88
|
+
// could revert and break the whole fee calculation
|
|
89
|
+
feeData = await estimateTxFee(delegatorAddress, to, null, data)
|
|
90
|
+
} catch {
|
|
91
|
+
console.warn('ETH unstake pending estimation reverted')
|
|
92
|
+
return Object.create(null)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { to, txData: data, ...feeData }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function getUndelegateData({ delegatorAddress, resquestedAmount, pendingAmount }) {
|
|
99
|
+
const canUnstake = resquestedAmount.gt(pendingAmount)
|
|
100
|
+
|
|
101
|
+
if (!canUnstake) return Object.create(null)
|
|
102
|
+
|
|
103
|
+
const activeAmountToUnstake = resquestedAmount.sub(pendingAmount)
|
|
104
|
+
const { to, data } = await staking.unstake({
|
|
105
|
+
address: delegatorAddress,
|
|
106
|
+
amount: activeAmountToUnstake,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const feeData = await estimateTxFee(delegatorAddress, to, null, data)
|
|
110
|
+
|
|
111
|
+
return { to, txData: data, ...feeData }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Unstaking may involve 2 txs; unstaking from pending and unstaking from validator.
|
|
116
|
+
* Fee estimation depends on the executed txs. Can be both.
|
|
117
|
+
* @returns total undelegete fee
|
|
118
|
+
*/
|
|
119
|
+
async function estimateUndelegate({ walletAccount, amount: resquestedAmount }) {
|
|
120
|
+
const address = await assetClientInterface.getReceiveAddress({
|
|
121
|
+
assetName: asset.name,
|
|
122
|
+
walletAccount,
|
|
123
|
+
})
|
|
124
|
+
const delegatorAddress = address.toLowerCase()
|
|
125
|
+
const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
|
|
126
|
+
|
|
127
|
+
const { fee: undelegatePendingFee = asset.currency.ZERO } = await getUndelegatePendingData({
|
|
128
|
+
delegatorAddress,
|
|
129
|
+
resquestedAmount,
|
|
130
|
+
pendingAmount,
|
|
131
|
+
minAmount,
|
|
132
|
+
})
|
|
133
|
+
const { fee: undelegateFee = asset.currency.ZERO } = await getUndelegateData({
|
|
134
|
+
delegatorAddress,
|
|
135
|
+
resquestedAmount,
|
|
136
|
+
pendingAmount,
|
|
137
|
+
minAmount,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return undelegatePendingFee.add(undelegateFee)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function undelegate({ walletAccount, amount } = Object.create(null)) {
|
|
66
144
|
/*
|
|
67
145
|
unstakePending balance (not yet in validator) + unstake balance (in validator)
|
|
68
146
|
1. give priority to unstakePending (based on the amount)
|
|
69
147
|
2. unstake amount in validator.
|
|
70
148
|
*/
|
|
71
|
-
|
|
149
|
+
const resquestedAmount = amountToCurrency({ asset, amount })
|
|
72
150
|
|
|
73
151
|
const address = await assetClientInterface.getReceiveAddress({
|
|
74
152
|
assetName: asset.name,
|
|
75
153
|
walletAccount,
|
|
76
154
|
})
|
|
77
155
|
const delegatorAddress = address.toLowerCase()
|
|
78
|
-
|
|
79
156
|
const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
|
|
80
157
|
|
|
81
158
|
console.log(
|
|
82
|
-
`delegator address ${delegatorAddress} unstaking ${
|
|
159
|
+
`delegator address ${delegatorAddress} unstaking ${resquestedAmount.toDefaultString({
|
|
83
160
|
unit: true,
|
|
84
161
|
})} - pending amount: ${pendingAmount.toDefaultString({ unit: true })}`
|
|
85
162
|
)
|
|
86
163
|
|
|
87
164
|
let txId
|
|
88
|
-
if (
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
throw new Error(`Pending balance less than min stake amount ${minStake}`)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const { to, data } = await staking.unstakePending({
|
|
99
|
-
address: delegatorAddress,
|
|
100
|
-
amount: inactiveAmountToUnstake,
|
|
165
|
+
if (pendingAmount.isPositive) {
|
|
166
|
+
const undelegatePendingData = await getUndelegatePendingData({
|
|
167
|
+
delegatorAddress,
|
|
168
|
+
resquestedAmount,
|
|
169
|
+
pendingAmount,
|
|
170
|
+
minAmount,
|
|
101
171
|
})
|
|
102
172
|
|
|
103
|
-
const { gasPrice, gasLimit, fee } = await estimateTxFee(
|
|
104
|
-
delegatorAddress.toLowerCase(),
|
|
105
|
-
to,
|
|
106
|
-
null,
|
|
107
|
-
data
|
|
108
|
-
)
|
|
109
173
|
txId = await prepareAndSendTx({
|
|
110
174
|
asset,
|
|
111
175
|
walletAccount,
|
|
112
|
-
|
|
113
|
-
txData: data,
|
|
114
|
-
gasPrice,
|
|
115
|
-
gasLimit,
|
|
116
|
-
fee,
|
|
176
|
+
...undelegatePendingData,
|
|
117
177
|
})
|
|
118
178
|
}
|
|
119
179
|
|
|
120
|
-
// need also to unstake
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
})
|
|
180
|
+
// may need also to unstake
|
|
181
|
+
const undelegateData = await getUndelegateData({
|
|
182
|
+
delegatorAddress,
|
|
183
|
+
resquestedAmount,
|
|
184
|
+
pendingAmount,
|
|
185
|
+
})
|
|
127
186
|
|
|
128
|
-
|
|
129
|
-
txId = await prepareAndSendTx({
|
|
130
|
-
asset,
|
|
131
|
-
walletAccount,
|
|
132
|
-
to,
|
|
133
|
-
txData: data,
|
|
134
|
-
gasPrice,
|
|
135
|
-
gasLimit,
|
|
136
|
-
fee,
|
|
137
|
-
})
|
|
138
|
-
}
|
|
187
|
+
if (!undelegateData) return txId
|
|
139
188
|
|
|
140
|
-
|
|
189
|
+
txId = await prepareAndSendTx({
|
|
190
|
+
asset,
|
|
191
|
+
walletAccount,
|
|
192
|
+
...undelegateData,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Testnet assets do not support delegations tracking
|
|
141
196
|
if (asset.name === 'ethereum')
|
|
142
197
|
await stakingProvider.notifyUnstaking({
|
|
143
198
|
txId,
|
|
144
199
|
asset: asset.name,
|
|
145
200
|
delegator: delegatorAddress,
|
|
146
|
-
amount:
|
|
201
|
+
amount: resquestedAmount.toBaseString(),
|
|
147
202
|
})
|
|
148
203
|
|
|
149
204
|
return txId
|
|
150
205
|
}
|
|
151
206
|
|
|
152
|
-
async function claimUndelegatedBalance({ walletAccount } =
|
|
153
|
-
//
|
|
207
|
+
async function claimUndelegatedBalance({ walletAccount } = Object.create(null)) {
|
|
208
|
+
// claim withdrawable balance (of a previous unstake)
|
|
154
209
|
const address = await assetClientInterface.getReceiveAddress({
|
|
155
210
|
assetName: asset.name,
|
|
156
211
|
walletAccount,
|
|
@@ -173,11 +228,17 @@ export function createEthereumStakingService({
|
|
|
173
228
|
fee,
|
|
174
229
|
})
|
|
175
230
|
}
|
|
176
|
-
|
|
177
|
-
return null // -> no withdrawable balance
|
|
178
231
|
}
|
|
179
232
|
|
|
180
233
|
async function estimateDelegateOperation({ walletAccount, operation, args }) {
|
|
234
|
+
const requestedAmount = args.amount
|
|
235
|
+
? amountToCurrency({ asset, amount: args.amount })
|
|
236
|
+
: asset.currency.ZERO
|
|
237
|
+
|
|
238
|
+
if (operation === 'undelegate') {
|
|
239
|
+
return estimateUndelegate({ walletAccount, amount: requestedAmount })
|
|
240
|
+
}
|
|
241
|
+
|
|
181
242
|
const address = await assetClientInterface.getReceiveAddress({
|
|
182
243
|
assetName: asset.name,
|
|
183
244
|
walletAccount,
|
|
@@ -189,30 +250,18 @@ export function createEthereumStakingService({
|
|
|
189
250
|
undelegate: 'unstake',
|
|
190
251
|
claimUndelegatedBalance: 'claimWithdrawRequest',
|
|
191
252
|
}
|
|
253
|
+
|
|
192
254
|
const delegateOperation = staking[NAMING_MAP[operation]].bind(staking)
|
|
193
255
|
|
|
194
256
|
if (!delegateOperation) {
|
|
195
|
-
|
|
257
|
+
throw new Error('Invalid staking operation')
|
|
196
258
|
}
|
|
197
259
|
|
|
198
|
-
|
|
199
|
-
|
|
260
|
+
const { amount, data } = await delegateOperation({ ...args, amount: requestedAmount })
|
|
261
|
+
const toAddress =
|
|
262
|
+
operation === 'claimUndelegatedBalance' ? staking.accountingAddress : staking.poolAddress
|
|
200
263
|
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
;({ amount, data } = await delegateOperation(args))
|
|
204
|
-
const address =
|
|
205
|
-
operation === 'claimUndelegatedBalance' ? staking.accountingAddress : staking.poolAddress
|
|
206
|
-
;({ fee } = await estimateTxFee(delegatorAddress, address, amount, data))
|
|
207
|
-
} catch (e) {
|
|
208
|
-
// If the operation is to unstake and failed retry with unstakePending
|
|
209
|
-
if (operation === 'undelegate') {
|
|
210
|
-
;({ amount, data } = await staking.unstakePending(args))
|
|
211
|
-
;({ fee } = await estimateTxFee(delegatorAddress, staking.poolAddress, amount, data))
|
|
212
|
-
} else {
|
|
213
|
-
throw e
|
|
214
|
-
}
|
|
215
|
-
}
|
|
264
|
+
const { fee } = await estimateTxFee(delegatorAddress, toAddress, amount, data)
|
|
216
265
|
|
|
217
266
|
return fee
|
|
218
267
|
}
|
|
@@ -247,16 +296,11 @@ export function createEthereumStakingService({
|
|
|
247
296
|
return staking.minAmount
|
|
248
297
|
}
|
|
249
298
|
|
|
250
|
-
async function prepareAndSendTx(
|
|
251
|
-
asset,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
txData: txInput,
|
|
256
|
-
gasPrice,
|
|
257
|
-
gasLimit,
|
|
258
|
-
fee,
|
|
259
|
-
} = {}) {
|
|
299
|
+
async function prepareAndSendTx(
|
|
300
|
+
{ asset, walletAccount, to, amount, txData: txInput, gasPrice, gasLimit, fee } = Object.create(
|
|
301
|
+
null
|
|
302
|
+
)
|
|
303
|
+
) {
|
|
260
304
|
const sendTxArgs = {
|
|
261
305
|
asset,
|
|
262
306
|
walletAccount,
|