@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 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.17.0",
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 eslint .",
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": "ea79042fc0d7ec1f4c87d3b0b511840b910ffa48"
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 undelegate({ walletAccount, amount } = {}) {
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
- amount = amountToCurrency({ asset, amount })
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 ${amount.toDefaultString({
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 (!pendingAmount.isZero) {
89
- const inactiveAmountToUnstake = pendingAmount.lte(amount) ? pendingAmount : amount
90
- const leftOver = pendingAmount.sub(amount)
91
- if (leftOver.gt(asset.currency.ZERO)) {
92
- const minStake = staking.minAmount
93
- if (leftOver.lt(minStake)) {
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
- to,
113
- txData: data,
114
- gasPrice,
115
- gasLimit,
116
- fee,
176
+ ...undelegatePendingData,
117
177
  })
118
178
  }
119
179
 
120
- // need also to unstake
121
- if (amount.gt(pendingAmount)) {
122
- const activeAmountToUnstake = amount.sub(pendingAmount)
123
- const { to, data } = await staking.unstake({
124
- address: delegatorAddress,
125
- amount: activeAmountToUnstake,
126
- })
180
+ // may need also to unstake
181
+ const undelegateData = await getUndelegateData({
182
+ delegatorAddress,
183
+ resquestedAmount,
184
+ pendingAmount,
185
+ })
127
186
 
128
- const { gasPrice, gasLimit, fee } = await estimateTxFee(delegatorAddress, to, null, data)
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
- // Goerli is not supported
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: amount.toBaseString(),
201
+ amount: resquestedAmount.toBaseString(),
147
202
  })
148
203
 
149
204
  return txId
150
205
  }
151
206
 
152
- async function claimUndelegatedBalance({ walletAccount } = {}) {
153
- // withdraw withdrawable balance (of a previous unstake)
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
- return
257
+ throw new Error('Invalid staking operation')
196
258
  }
197
259
 
198
- // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
199
- if (args.amount) args.amount = amountToCurrency({ asset, amount: args.amount })
260
+ const { amount, data } = await delegateOperation({ ...args, amount: requestedAmount })
261
+ const toAddress =
262
+ operation === 'claimUndelegatedBalance' ? staking.accountingAddress : staking.poolAddress
200
263
 
201
- let amount, data, fee
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
- walletAccount,
253
- to,
254
- amount,
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,