77
88 "github.com/ethereum/go-ethereum/accounts/abi/bind"
99 "github.com/ethereum/go-ethereum/common"
10+ gethtypes "github.com/ethereum/go-ethereum/core/types"
1011 "github.com/urfave/cli/v3"
1112 "golang.org/x/sync/errgroup"
1213
@@ -18,9 +19,13 @@ import (
1819
1920 "github.com/rocket-pool/smartnode/shared/services"
2021 "github.com/rocket-pool/smartnode/shared/services/beacon"
22+ "github.com/rocket-pool/smartnode/shared/services/flashbots"
2123 "github.com/rocket-pool/smartnode/shared/types/api"
2224)
2325
26+ // Gas can't be estimated against current chain state (the fee distributor's distribute() in the bundle hasn't executed yet)
27+ const distributeBalanceBundleGasLimit uint64 = 600000
28+
2429func getMinipoolCloseDetailsForNode (c * cli.Command ) (* api.GetMinipoolCloseDetailsForNodeResponse , error ) {
2530
2631 // Get services
@@ -307,7 +312,7 @@ func getMinipoolCloseDetails(rp *rocketpool.RocketPool, minipoolAddress common.A
307312
308313}
309314
310- func closeMinipool (c * cli.Command , minipoolAddress common.Address , opts * bind.TransactOpts ) (* api.CloseMinipoolResponse , error ) {
315+ func closeMinipool (c * cli.Command , minipoolAddress common.Address , opts * bind.TransactOpts , bundle bool ) (* api.CloseMinipoolResponse , error ) {
311316
312317 // Get services
313318 if err := services .RequireNodeRegistered (c ); err != nil {
@@ -373,12 +378,91 @@ func closeMinipool(c *cli.Command, minipoolAddress common.Address, opts *bind.Tr
373378 }
374379 response .TxHash = hash
375380 } else {
376- // Do a distribution, which will finalize it
377- hash , err := mpv3 .DistributeBalance (false , opts )
381+ cfg , err := services .GetConfig (c )
378382 if err != nil {
379- return nil , err
383+ return nil , fmt . Errorf ( "error getting config: %w" , err )
380384 }
381- response .TxHash = hash
385+
386+ ec , err := services .GetEthClient (c )
387+ if err != nil {
388+ return nil , fmt .Errorf ("error getting eth client: %w" , err )
389+ }
390+
391+ relayUrl := cfg .Smartnode .GetFlashbotsRelayUrl ()
392+ useBundle := bundle
393+ if useBundle && relayUrl == "" {
394+ return nil , fmt .Errorf ("a bundle was requested but Flashbots bundles are only supported on mainnet; there is no relay for this network" )
395+ }
396+
397+ var distributorAddress common.Address
398+ if relayUrl != "" {
399+ w , err := services .GetWallet (c )
400+ if err != nil {
401+ return nil , err
402+ }
403+ nodeAccount , err := w .GetNodeAccount ()
404+ if err != nil {
405+ return nil , err
406+ }
407+ distributorAddress , err = node .GetDistributorAddress (rp , nodeAccount .Address , nil )
408+ if err != nil {
409+ return nil , fmt .Errorf ("error getting fee distributor address: %w" , err )
410+ }
411+
412+ if ! useBundle {
413+ distributorBalance , err := ec .BalanceAt (context .Background (), distributorAddress , nil )
414+ if err != nil {
415+ return nil , fmt .Errorf ("error getting fee distributor balance: %w" , err )
416+ }
417+ useBundle = distributorBalance .Cmp (big .NewInt (1 )) == 0
418+ }
419+ }
420+
421+ if ! useBundle {
422+ // Do a plain distribution, which will finalize it
423+ hash , err := mpv3 .DistributeBalance (false , opts )
424+ if err != nil {
425+ return nil , err
426+ }
427+ response .TxHash = hash
428+ return & response , nil
429+ }
430+
431+ // Empty the fee distributor and distribute the minipool balance (which also finalizes
432+ // it) atomically in the same block via a Flashbots bundle.
433+ distributor , err := node .NewDistributor (rp , distributorAddress , nil )
434+ if err != nil {
435+ return nil , fmt .Errorf ("error creating fee distributor binding: %w" , err )
436+ }
437+
438+ // First tx: fee distributor distribute()
439+ distributorTx , err := distributor .PrepareDistribute (opts )
440+ if err != nil {
441+ return nil , fmt .Errorf ("error preparing fee distributor distribute tx for bundle: %w" , err )
442+ }
443+
444+ // Second tx: minipool distributeBalance(), with bumped nonce and a fixed gas limit
445+ opts .Nonce = new (big.Int ).SetUint64 (distributorTx .Nonce () + 1 )
446+ opts .GasLimit = distributeBalanceBundleGasLimit
447+
448+ distBalTx , err := mpv3 .PrepareDistributeBalance (false , opts )
449+ if err != nil {
450+ return nil , fmt .Errorf ("error preparing distribute balance tx for bundle on minipool %s: %w" , minipoolAddress .Hex (), err )
451+ }
452+
453+ // Send the 2-tx bundle (distribute then distributeBalance)
454+ timeoutCtx , cancel := context .WithTimeout (context .Background (), flashbots .DefaultSubmissionTimeout )
455+ success , err := flashbots .SubmitBundleAndWait (timeoutCtx , nil , ec , relayUrl , []* gethtypes.Transaction {distributorTx , distBalTx }, flashbots .DefaultBundleBlockCount )
456+ cancel ()
457+ if err != nil {
458+ return nil , fmt .Errorf ("error sending bundle for distribute+distributeBalance: %w" , err )
459+ }
460+ if ! success {
461+ return nil , fmt .Errorf ("bundle for minipool %s distribute+distributeBalance was not included. Bundles usually require a higher priority fee to get included" , minipoolAddress .Hex ())
462+ }
463+
464+ // Report the distributeBalance tx hash (the last tx in the bundle).
465+ response .TxHash = distBalTx .Hash ()
382466 }
383467
384468 // Return response
0 commit comments