@exodus/ethereum-api 10.0.0 → 10.1.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.
@@ -66,11 +66,12 @@ import NumberUnit from '@exodus/currency'
66
66
  import assert from 'minimalistic-assert'
67
67
 
68
68
  import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
69
- import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
69
+ import { createStakingTxs, sendStakingTxs, STAKING_TX_MODE } from '../evm/staking-txs.js'
70
70
  import { stakingProviderClientFactory } from '../staking-provider-client.js'
71
71
  import { amountToCurrency, DISABLE_BALANCE_CHECKS, resolveFeeData } from '../utils/index.js'
72
72
  import { EthereumStaking } from './api.js'
73
73
  import { getCachedEverstakeValidatorsQueue } from './everstake.js'
74
+ import { ETH_STAKING_MIN_AMOUNT } from './min-amount.js'
74
75
  import { simulateUndelegateTransactions } from './staking-utils.js'
75
76
 
76
77
  const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
@@ -174,7 +175,6 @@ export function createEthereumStakingService({
174
175
  asset: deprectedArg, // @deprecated use `assetName` instead
175
176
  assetName,
176
177
  assetClientInterface,
177
- createWatchTx = defaultCreateWatch,
178
178
  stakingProvider = stakingProviderClientFactory(),
179
179
  }) {
180
180
  if (deprectedArg && !assetName) {
@@ -202,7 +202,21 @@ export function createEthereumStakingService({
202
202
  return { delegatorAddress, feeData }
203
203
  }
204
204
 
205
- async function delegate({ walletAccount, amount, feeData, isDelegateAll }) {
205
+ const createPlanTxs = async ({ walletAccount, plan }) => {
206
+ const createdTxs = await createStakingTxs({
207
+ asset: plan.asset,
208
+ walletAccount,
209
+ fromAddress: plan.delegatorAddress,
210
+ steps: plan.steps,
211
+ mode: plan.mode,
212
+ gasPrice: plan.gasPrice,
213
+ tipGasPrice: plan.tipGasPrice,
214
+ feeData: plan.feeData,
215
+ })
216
+ return createdTxs.map(({ unsignedTx }) => unsignedTx)
217
+ }
218
+
219
+ async function planDelegate({ walletAccount, amount, feeData, isDelegateAll }) {
206
220
  const asset = await getAsset(assetName)
207
221
  const staking = getStakingApi(asset)
208
222
  amount = amountToCurrency({ asset, amount })
@@ -228,24 +242,40 @@ export function createEthereumStakingService({
228
242
 
229
243
  // If we are delegating all, we need to subtract the estimated ETH cost from the amount
230
244
  // Better to do it here than from the mobile app where fees can change before the transaction is sent
231
- if (isDelegateAll) {
232
- const minAmount = getMinAmount(asset)
233
- amount = amount.sub(fee)
234
- // Re-validate that the amount after fee subtraction is still above the minimum
235
- assert(amount.gte(minAmount), `Min Amount ${minAmount}`)
236
- }
245
+ if (isDelegateAll) amount = amount.sub(fee)
237
246
 
238
- const txId = await prepareAndSendTx({
247
+ return {
239
248
  asset,
240
- walletAccount,
241
- to,
249
+ delegatorAddress,
242
250
  amount,
243
- txData: data,
251
+ fee,
252
+ feeData,
244
253
  gasPrice,
245
- gasLimit,
246
254
  tipGasPrice,
247
- feeData,
248
- })
255
+ steps: [{ to, txInput: data, amount, gasLimit }],
256
+ mode: STAKING_TX_MODE.single,
257
+ }
258
+ }
259
+
260
+ async function buildDelegate(params) {
261
+ const plan = await planDelegate(params)
262
+ if (params.isDelegateAll) {
263
+ const minAmount = getMinAmount(plan.asset)
264
+ // Re-validate that the amount after fee subtraction is still above the minimum
265
+ assert(plan.amount.gte(minAmount), `Min Amount ${minAmount}`)
266
+ }
267
+
268
+ const unsignedTxs = await createPlanTxs({ walletAccount: params.walletAccount, plan })
269
+ return { ...plan, unsignedTxs }
270
+ }
271
+
272
+ async function delegate({ walletAccount, amount, feeData, isDelegateAll }) {
273
+ const plan = await buildDelegate({ walletAccount, amount, feeData, isDelegateAll })
274
+ const { asset, delegatorAddress, unsignedTxs, mode } = plan
275
+ amount = plan.amount
276
+ const {
277
+ txIds: [txId],
278
+ } = await sendStakingTxs({ asset, walletAccount, unsignedTxs, mode })
249
279
 
250
280
  // Goerli is not supported
251
281
  if (assetName === 'ethereum') {
@@ -276,11 +306,6 @@ export function createEthereumStakingService({
276
306
  ])
277
307
  const delegatorAddress = address.toLowerCase()
278
308
  const pendingAmount = await staking.pendingBalanceOf(delegatorAddress)
279
- const baseNonce = await asset.baseAsset.getNonce({
280
- asset,
281
- fromAddress: delegatorAddress,
282
- walletAccount,
283
- })
284
309
 
285
310
  return {
286
311
  asset,
@@ -290,7 +315,6 @@ export function createEthereumStakingService({
290
315
  feeData: resolvedFeeData,
291
316
  delegatorAddress,
292
317
  pendingAmount,
293
- baseNonce,
294
318
  }
295
319
  }
296
320
 
@@ -340,12 +364,7 @@ export function createEthereumStakingService({
340
364
  return pendingFee.add(undelegateFee)
341
365
  }
342
366
 
343
- // NOTE: Like the legacy `undelegate()` flow, this planner currently splits
344
- // the request using `pendingAmount` only. It does not pre-validate that the
345
- // validator-side remainder is actually available in active staked balance,
346
- // so an oversized request can still fail later when building/sending the
347
- // validator unstake step.
348
- async function undelegate({ walletAccount, amount, feeData, revertOnSimulationError = true }) {
367
+ async function planUndelegate({ walletAccount, amount, feeData }) {
349
368
  const {
350
369
  asset,
351
370
  staking,
@@ -354,7 +373,6 @@ export function createEthereumStakingService({
354
373
  feeData: resolvedFeeData,
355
374
  delegatorAddress,
356
375
  pendingAmount,
357
- baseNonce,
358
376
  } = await prepareUndelegate({ walletAccount, amount, feeData })
359
377
 
360
378
  if (!requestedAmount.isPositive) {
@@ -423,48 +441,68 @@ export function createEthereumStakingService({
423
441
  tipGasPrice = tipGasPrice || undelegateResult.feeInfo.tipGasPrice
424
442
  }
425
443
 
426
- // 3. Create unsigned txs
427
- const createTxBaseArgs = {
444
+ const plannedSteps = Object.values(txSteps).filter((txStep) => txStep.plan)
445
+ const fee = [pendingResult, undelegateResult]
446
+ .filter(Boolean)
447
+ .reduce((total, { feeInfo }) => total.add(feeInfo.fee), asset.currency.ZERO)
448
+
449
+ return {
428
450
  asset,
429
- walletAccount,
430
- fromAddress: delegatorAddress,
431
- amount: asset.currency.ZERO,
432
- tipGasPrice,
451
+ delegatorAddress,
452
+ amount: requestedAmount,
453
+ fee,
454
+ feeData: resolvedFeeData,
433
455
  gasPrice,
456
+ tipGasPrice,
457
+ txSteps,
458
+ steps: plannedSteps.map((txStep) => ({
459
+ to: txStep.plan.to,
460
+ txInput: txStep.plan.txData,
461
+ gasLimit: txStep.gasLimit,
462
+ })),
463
+ mode: STAKING_TX_MODE.bundle,
434
464
  }
465
+ }
435
466
 
436
- const createdTxEntries = await Promise.all(
437
- Object.entries(txSteps)
438
- .filter(([, txStep]) => txStep.plan)
439
- .map(async ([key, txStep], index) => {
440
- const result = await asset.baseAsset.api.createTx({
441
- ...createTxBaseArgs,
442
- toAddress: txStep.plan.to,
443
- txInput: txStep.plan.txData,
444
- gasLimit: txStep.gasLimit,
445
- nonce: baseNonce + index,
446
- })
447
- return [key, result.unsignedTx]
448
- })
467
+ async function simulateUndelegate({ plan, unsignedTxs, revertOnSimulationError = true }) {
468
+ const plannedSteps = Object.values(plan.txSteps).filter((txStep) => txStep.plan)
469
+ const txSteps = Object.fromEntries(
470
+ Object.entries(plan.txSteps).map(([key, txStep]) => [
471
+ key,
472
+ { ...txStep, unsignedTx: unsignedTxs[plannedSteps.indexOf(txStep)] ?? null },
473
+ ])
449
474
  )
450
475
 
451
- for (const [key, unsignedTx] of createdTxEntries) {
452
- txSteps[key].unsignedTx = unsignedTx
453
- }
454
-
455
476
  await simulateUndelegateTransactions({
456
- asset,
477
+ asset: plan.asset,
457
478
  txSteps,
458
- senderAddress: delegatorAddress,
459
- gasPrice,
479
+ senderAddress: plan.delegatorAddress,
480
+ gasPrice: plan.gasPrice,
460
481
  revertOnSimulationError,
461
482
  })
462
483
 
463
- const unsignedTxBundle = createdTxEntries.map(([_, unsignedTx]) => ({
464
- asset,
465
- unsignedTx,
484
+ return txSteps
485
+ }
486
+
487
+ async function buildUndelegate({ walletAccount, amount, feeData, revertOnSimulationError }) {
488
+ const plan = await planUndelegate({ walletAccount, amount, feeData })
489
+ const unsignedTxs = await createPlanTxs({ walletAccount, plan })
490
+ const txSteps = await simulateUndelegate({ plan, unsignedTxs, revertOnSimulationError })
491
+ return { ...plan, txSteps, unsignedTxs }
492
+ }
493
+
494
+ // NOTE: Like the legacy `undelegate()` flow, this planner currently splits
495
+ // the request using `pendingAmount` only. It does not pre-validate that the
496
+ // validator-side remainder is actually available in active staked balance,
497
+ // so an oversized request can still fail later when building/sending the
498
+ // validator unstake step.
499
+ async function undelegate({ walletAccount, amount, feeData, revertOnSimulationError = true }) {
500
+ const { asset, delegatorAddress, txSteps, unsignedTxs, mode } = await buildUndelegate({
466
501
  walletAccount,
467
- }))
502
+ amount,
503
+ feeData,
504
+ revertOnSimulationError,
505
+ })
468
506
 
469
507
  // 4. Sign transactions
470
508
  //
@@ -498,7 +536,7 @@ export function createEthereumStakingService({
498
536
  const {
499
537
  bundleHash,
500
538
  txIds: [...txIds],
501
- } = await asset.baseAsset.sendPrivateBundle({ unsignedTxBundle })
539
+ } = await sendStakingTxs({ asset, walletAccount, unsignedTxs, mode })
502
540
 
503
541
  for (const txStep of Object.values(txSteps)) {
504
542
  if (!txStep.unsignedTx) continue
@@ -525,7 +563,7 @@ export function createEthereumStakingService({
525
563
  return bundleHash
526
564
  }
527
565
 
528
- async function claimUndelegatedBalance({ walletAccount, feeData }) {
566
+ async function planClaimUndelegatedBalance({ walletAccount, feeData }) {
529
567
  const asset = await getAsset(assetName)
530
568
  const staking = getStakingApi(asset)
531
569
 
@@ -536,29 +574,52 @@ export function createEthereumStakingService({
536
574
  address: delegatorAddress,
537
575
  })
538
576
 
539
- if (withdrawRequest) {
540
- const { to, data } = withdrawRequest
577
+ if (!withdrawRequest) return null
541
578
 
542
- const { gasLimit, gasPrice, tipGasPrice } = await estimateTxFee({
543
- from: delegatorAddress,
544
- to,
545
- amount: null,
546
- txInput: data,
547
- feeData,
548
- })
549
- return prepareAndSendTx({
550
- asset,
551
- walletAccount,
552
- to,
553
- txData: data,
554
- gasLimit,
555
- gasPrice,
556
- tipGasPrice,
557
- feeData,
558
- })
579
+ const { to, data } = withdrawRequest
580
+ const { gasLimit, gasPrice, tipGasPrice, fee } = await estimateTxFee({
581
+ from: delegatorAddress,
582
+ to,
583
+ amount: null,
584
+ txInput: data,
585
+ feeData,
586
+ })
587
+
588
+ return {
589
+ asset,
590
+ delegatorAddress,
591
+ fee,
592
+ feeData,
593
+ gasPrice,
594
+ tipGasPrice,
595
+ steps: [{ to, txInput: data, gasLimit }],
596
+ mode: STAKING_TX_MODE.single,
559
597
  }
560
598
  }
561
599
 
600
+ async function buildClaimUndelegatedBalance(params) {
601
+ const plan = await planClaimUndelegatedBalance(params)
602
+ if (!plan) return null
603
+
604
+ const unsignedTxs = await createPlanTxs({ walletAccount: params.walletAccount, plan })
605
+ return { ...plan, unsignedTxs }
606
+ }
607
+
608
+ async function claimUndelegatedBalance({ walletAccount, feeData }) {
609
+ const plan = await buildClaimUndelegatedBalance({ walletAccount, feeData })
610
+ if (!plan) return
611
+
612
+ const {
613
+ txIds: [txId],
614
+ } = await sendStakingTxs({
615
+ asset: plan.asset,
616
+ walletAccount,
617
+ unsignedTxs: plan.unsignedTxs,
618
+ mode: plan.mode,
619
+ })
620
+ return txId
621
+ }
622
+
562
623
  async function estimateDelegateOperation({ walletAccount, operation, args, feeData }) {
563
624
  const asset = await getAsset(assetName)
564
625
 
@@ -723,52 +784,16 @@ export function createEthereumStakingService({
723
784
  }
724
785
  }
725
786
 
726
- async function prepareAndSendTx({
727
- asset,
728
- walletAccount,
729
- to,
730
- amount,
731
- txData: txInput,
732
- gasPrice,
733
- gasLimit,
734
- tipGasPrice,
735
- waitForConfirmation = false,
736
- feeData,
737
- }) {
738
- const { unsignedTx } = await asset.baseAsset.api.createTx({
739
- asset,
740
- walletAccount,
741
- address: to,
742
- amount: amount || asset.currency.ZERO,
743
- txInput,
744
- gasPrice,
745
- gasLimit,
746
- // HACK: Override the `tipGasPrice` to use a custom `maxPriorityFeePerGas`.
747
- tipGasPrice,
748
- feeData,
749
- })
750
-
751
- const { txId } = await asset.baseAsset.api.sendTx({ asset, walletAccount, unsignedTx })
752
-
753
- const baseAsset = asset.baseAsset
754
- if (waitForConfirmation) {
755
- const getTxLog = (...args) => assetClientInterface.getTxLog(...args)
756
- const watchTx = createWatchTx({
757
- walletAccount,
758
- getTxLog,
759
- baseAsset,
760
- })
761
- await watchTx(txId)
762
- }
763
-
764
- return txId
765
- }
766
-
767
787
  return {
788
+ planDelegate,
789
+ planUndelegate,
790
+ planClaimUndelegatedBalance,
791
+ simulateUndelegate,
792
+ getEthereumStakingInfo,
793
+ // @deprecated legacy staking api, remove once evm staking unification is done
768
794
  delegate,
769
795
  undelegate,
770
796
  claimUndelegatedBalance,
771
- getEthereumStakingInfo,
772
797
  estimateDelegateOperation,
773
798
  getMinAmount,
774
799
  getDelegateSelectAllAmount,
@@ -801,7 +826,7 @@ export async function getEthereumStakingInfo({
801
826
  ])
802
827
 
803
828
  const delegatedBalance = activeStakedBalance.add(pendingBalance).add(pendingDepositedBalance)
804
- const minDelegateAmount = currency.defaultUnit(0.1)
829
+ const minDelegateAmount = currency.defaultUnit(ETH_STAKING_MIN_AMOUNT)
805
830
 
806
831
  const {
807
832
  requested,