@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.
- package/CHANGELOG.md +18 -0
- package/package.json +3 -3
- package/src/create-asset-utils.js +6 -6
- package/src/create-asset.js +32 -10
- package/src/create-token-factory.js +18 -7
- package/src/index.js +15 -0
- package/src/staking/api/index.js +2 -1
- package/src/staking/ethereum/api.js +2 -3
- package/src/staking/ethereum/deps.js +3 -0
- package/src/staking/ethereum/min-amount.js +1 -0
- package/src/staking/ethereum/service.js +148 -123
- package/src/staking/ethereum/staking-adapter.js +382 -0
- package/src/staking/evm/compose-staking-api.js +42 -0
- package/src/staking/evm/create-evm-staking-api.js +269 -0
- package/src/staking/evm/staking-errors.js +37 -0
- package/src/staking/evm/staking-txs.js +107 -0
- package/src/staking/matic/index.js +1 -0
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
247
|
+
return {
|
|
239
248
|
asset,
|
|
240
|
-
|
|
241
|
-
to,
|
|
249
|
+
delegatorAddress,
|
|
242
250
|
amount,
|
|
243
|
-
|
|
251
|
+
fee,
|
|
252
|
+
feeData,
|
|
244
253
|
gasPrice,
|
|
245
|
-
gasLimit,
|
|
246
254
|
tipGasPrice,
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
const
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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(
|
|
829
|
+
const minDelegateAmount = currency.defaultUnit(ETH_STAKING_MIN_AMOUNT)
|
|
805
830
|
|
|
806
831
|
const {
|
|
807
832
|
requested,
|