From 856d4cef87efa3d0757bdcb4fcd6a362e26656e4 Mon Sep 17 00:00:00 2001 From: pk910 Date: Thu, 31 Oct 2024 19:58:28 +0100 Subject: [PATCH 01/13] start implementation of submit consolidations page --- .hack/devnet/run.sh | 1 + cmd/dora-explorer/main.go | 1 + config/default.config.yml | 1 + handlers/pageData.go | 25 +- handlers/submit_consolidation.go | 159 +++++++++ indexer/execution/consolidation_indexer.go | 4 +- indexer/execution/withdrawal_indexer.go | 4 +- .../submit_consolidation.html | 68 ++++ test-config.yaml | 1 + types/config.go | 7 +- types/models/submit_consolidation.go | 17 + ui-package/package-lock.json | 314 +++++++++++++++++- ui-package/package.json | 1 + .../ConsolidationReview.tsx | 22 ++ .../SubmitConsolidationsForm.scss | 24 ++ .../SubmitConsolidationsForm.tsx | 204 ++++++++++++ .../SubmitConsolidationsFormProps.ts | 13 + .../ValidatorSelector.tsx | 85 +++++ ui-package/src/main.tsx | 13 + 19 files changed, 945 insertions(+), 19 deletions(-) create mode 100644 handlers/submit_consolidation.go create mode 100644 templates/submit_consolidation/submit_consolidation.html create mode 100644 types/models/submit_consolidation.go create mode 100644 ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx create mode 100644 ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss create mode 100644 ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx create mode 100644 ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts create mode 100644 ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx diff --git a/.hack/devnet/run.sh b/.hack/devnet/run.sh index 852bf21a..685675b3 100755 --- a/.hack/devnet/run.sh +++ b/.hack/devnet/run.sh @@ -65,6 +65,7 @@ frontend: validatorNamesYaml: "${__dir}/generated-validator-ranges.yaml" showSensitivePeerInfos: true showSubmitDeposit: true + showSubmitConsolidation: true beaconapi: localCacheSize: 10 redisCacheAddr: "" diff --git a/cmd/dora-explorer/main.go b/cmd/dora-explorer/main.go index 84964543..568cac98 100644 --- a/cmd/dora-explorer/main.go +++ b/cmd/dora-explorer/main.go @@ -166,6 +166,7 @@ func startFrontend(webserver *http.Server) { router.HandleFunc("/validators/slashings", handlers.Slashings).Methods("GET") router.HandleFunc("/validators/el_withdrawals", handlers.ElWithdrawals).Methods("GET") router.HandleFunc("/validators/el_consolidations", handlers.ElConsolidations).Methods("GET") + router.HandleFunc("/validators/submit_consolidations", handlers.SubmitConsolidation).Methods("GET") router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET") router.HandleFunc("/validator/{index}/slots", handlers.ValidatorSlots).Methods("GET") diff --git a/config/default.config.yml b/config/default.config.yml index 8e668573..4a950432 100644 --- a/config/default.config.yml +++ b/config/default.config.yml @@ -35,6 +35,7 @@ frontend: showSensitivePeerInfos: false showPeerDASInfos: false showSubmitDeposit: false + showSubmitConsolidation: false beaconapi: # beacon node rpc endpoints diff --git a/handlers/pageData.go b/handlers/pageData.go index a6cef2b6..5585dba5 100644 --- a/handlers/pageData.go +++ b/handlers/pageData.go @@ -207,15 +207,26 @@ func createMenuItems(active string) []types.MainMenuItem { }, }) + submitLinks := []types.NavigationLink{} if utils.Config.Frontend.ShowSubmitDeposit { + submitLinks = append(submitLinks, types.NavigationLink{ + Label: "Submit Deposits", + Path: "/validators/deposits/submit", + Icon: "fa-file-import", + }) + } + + if utils.Config.Frontend.ShowSubmitConsolidation { + submitLinks = append(submitLinks, types.NavigationLink{ + Label: "Submit Consolidations", + Path: "/validators/submit_consolidations", + Icon: "fa-square-plus", + }) + } + + if len(submitLinks) > 0 { validatorMenu = append(validatorMenu, types.NavigationGroup{ - Links: []types.NavigationLink{ - { - Label: "Submit Deposits", - Path: "/validators/deposits/submit", - Icon: "fa-file-import", - }, - }, + Links: submitLinks, }) } diff --git a/handlers/submit_consolidation.go b/handlers/submit_consolidation.go new file mode 100644 index 00000000..b37375dd --- /dev/null +++ b/handlers/submit_consolidation.go @@ -0,0 +1,159 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/ethereum/go-ethereum/common" + "github.com/sirupsen/logrus" + + "github.com/ethpandaops/dora/indexer/execution" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" +) + +// SubmitConsolidation will submit a consolidation request +func SubmitConsolidation(w http.ResponseWriter, r *http.Request) { + var submitConsolidationTemplateFiles = append(layoutTemplateFiles, + "submit_consolidation/submit_consolidation.html", + ) + var pageTemplate = templates.GetTemplate(submitConsolidationTemplateFiles...) + + if !utils.Config.Frontend.ShowSubmitConsolidation { + handlePageError(w, r, errors.New("submit consolidation is not enabled")) + return + } + + query := r.URL.Query() + if query.Has("ajax") { + err := handleSubmitConsolidationPageDataAjax(w, r) + if err != nil { + handlePageError(w, r, err) + } + return + } + + pageData, pageError := getSubmitConsolidationPageData() + if pageError != nil { + handlePageError(w, r, pageError) + return + } + if pageData == nil { + data := InitPageData(w, r, "blockchain", "/submit_consolidation", "Submit Consolidation", submitConsolidationTemplateFiles) + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "submit_consolidation.go", "Submit Consolidation", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } + return + } + + data := InitPageData(w, r, "blockchain", "/submit_consolidation", "Submit Consolidation", submitConsolidationTemplateFiles) + data.Data = pageData + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "submit_consolidation.go", "Submit Consolidation", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getSubmitConsolidationPageData() (*models.SubmitConsolidationPageData, error) { + pageData := &models.SubmitConsolidationPageData{} + pageCacheKey := "submit_consolidation" + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} { + pageData, cacheTimeout := buildSubmitConsolidationPageData() + pageCall.CacheTimeout = cacheTimeout + return pageData + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.SubmitConsolidationPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildSubmitConsolidationPageData() (*models.SubmitConsolidationPageData, time.Duration) { + logrus.Debugf("submit consolidation page called") + + chainState := services.GlobalBeaconService.GetChainState() + specs := chainState.GetSpecs() + + pageData := &models.SubmitConsolidationPageData{ + NetworkName: specs.ConfigName, + PublicRPCUrl: utils.Config.Frontend.PublicRPCUrl, + RainbowkitProjectId: utils.Config.Frontend.RainbowkitProjectId, + ChainId: specs.DepositChainId, + ConsolidationContract: execution.ConsolidationContractAddr, + } + + return pageData, 1 * time.Hour +} + +func handleSubmitConsolidationPageDataAjax(w http.ResponseWriter, r *http.Request) error { + query := r.URL.Query() + var pageData interface{} + + switch query.Get("ajax") { + case "load_validators": + address := query.Get("address") + addressBytes := common.HexToAddress(address) + + validators := services.GlobalBeaconService.GetCachedValidatorSet() + result := []models.SubmitConsolidationPageDataValidator{} + for _, validator := range validators { + if validator.Validator.WithdrawalCredentials[0] == 0x00 && false { + continue + } + + if !bytes.Equal(validator.Validator.WithdrawalCredentials[12:], addressBytes[:]) && false { + continue + } + + var status string + if strings.HasPrefix(validator.Status.String(), "pending") { + status = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + status = "Active" + } else if validator.Status == v1.ValidatorStateActiveExiting { + status = "Exiting" + } else if validator.Status == v1.ValidatorStateActiveSlashed { + status = "Slashed" + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + status = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + status = "Slashed" + } else { + status = validator.Status.String() + } + + result = append(result, models.SubmitConsolidationPageDataValidator{ + Index: uint64(validator.Index), + Pubkey: validator.Validator.PublicKey.String(), + Balance: uint64(validator.Balance), + CredType: fmt.Sprintf("%x", validator.Validator.WithdrawalCredentials[0]), + Status: status, + }) + } + + pageData = result + default: + return errors.New("invalid ajax request") + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(pageData) + if err != nil { + logrus.WithError(err).Error("error encoding index data") + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + } + return nil +} diff --git a/indexer/execution/consolidation_indexer.go b/indexer/execution/consolidation_indexer.go index a467534c..498a18bf 100644 --- a/indexer/execution/consolidation_indexer.go +++ b/indexer/execution/consolidation_indexer.go @@ -17,7 +17,7 @@ import ( "github.com/ethpandaops/dora/utils" ) -const consolidationContractAddr = "0x01aBEa29659e5e97C95107F20bb753cD3e09bBBb" +const ConsolidationContractAddr = "0x01aBEa29659e5e97C95107F20bb753cD3e09bBBb" // ConsolidationIndexer is the indexer for the eip-7251 consolidation system contract type ConsolidationIndexer struct { @@ -54,7 +54,7 @@ func NewConsolidationIndexer(indexer *IndexerCtx) *ConsolidationIndexer { &contractIndexerOptions[dbtypes.ConsolidationRequestTx]{ stateKey: "indexer.consolidationindexer", batchSize: batchSize, - contractAddress: common.HexToAddress(consolidationContractAddr), + contractAddress: common.HexToAddress(ConsolidationContractAddr), deployBlock: uint64(utils.Config.ExecutionApi.ElectraDeployBlock), dequeueRate: specs.MaxConsolidationRequestsPerPayload, diff --git a/indexer/execution/withdrawal_indexer.go b/indexer/execution/withdrawal_indexer.go index a4f272bf..0da38222 100644 --- a/indexer/execution/withdrawal_indexer.go +++ b/indexer/execution/withdrawal_indexer.go @@ -18,7 +18,7 @@ import ( "github.com/ethpandaops/dora/utils" ) -const withdrawalContractAddr = "0x09Fc772D0857550724b07B850a4323f39112aAaA" +const WithdrawalContractAddr = "0x09Fc772D0857550724b07B850a4323f39112aAaA" // WithdrawalIndexer is the indexer for the eip-7002 consolidation system contract type WithdrawalIndexer struct { @@ -55,7 +55,7 @@ func NewWithdrawalIndexer(indexer *IndexerCtx) *WithdrawalIndexer { &contractIndexerOptions[dbtypes.WithdrawalRequestTx]{ stateKey: "indexer.withdrawalindexer", batchSize: batchSize, - contractAddress: common.HexToAddress(withdrawalContractAddr), + contractAddress: common.HexToAddress(WithdrawalContractAddr), deployBlock: uint64(utils.Config.ExecutionApi.ElectraDeployBlock), dequeueRate: specs.MaxWithdrawalRequestsPerPayload, diff --git a/templates/submit_consolidation/submit_consolidation.html b/templates/submit_consolidation/submit_consolidation.html new file mode 100644 index 00000000..bddac115 --- /dev/null +++ b/templates/submit_consolidation/submit_consolidation.html @@ -0,0 +1,68 @@ +{{ define "page" }} +
+
+

+ Submit Consolidation +

+ +
+ +
+ +
+
+ +
+
+
+
+ +{{ end }} +{{ define "js" }} + + +{{ end }} +{{ define "css" }} +{{ end }} \ No newline at end of file diff --git a/test-config.yaml b/test-config.yaml index 668f1cdf..1ba6be99 100644 --- a/test-config.yaml +++ b/test-config.yaml @@ -34,6 +34,7 @@ frontend: showSensitivePeerInfos: true showPeerDASInfos: true showSubmitDeposit: true + showSubmitConsolidation: true beaconapi: diff --git a/types/config.go b/types/config.go index 3bfa6fe4..46bb2cf6 100644 --- a/types/config.go +++ b/types/config.go @@ -51,9 +51,10 @@ type Config struct { HttpIdleTimeout time.Duration `yaml:"httpIdleTimeout" envconfig:"FRONTEND_HTTP_IDLE_TIMEOUT"` AllowDutyLoading bool `yaml:"allowDutyLoading" envconfig:"FRONTEND_ALLOW_DUTY_LOADING"` - ShowSensitivePeerInfos bool `yaml:"showSensitivePeerInfos" envconfig:"FRONTEND_SHOW_SENSITIVE_PEER_INFOS"` - ShowPeerDASInfos bool `yaml:"showPeerDASInfos" envconfig:"FRONTEND_SHOW_PEER_DAS_INFOS"` - ShowSubmitDeposit bool `yaml:"showSubmitDeposit" envconfig:"FRONTEND_SHOW_SUBMIT_DEPOSIT"` + ShowSensitivePeerInfos bool `yaml:"showSensitivePeerInfos" envconfig:"FRONTEND_SHOW_SENSITIVE_PEER_INFOS"` + ShowPeerDASInfos bool `yaml:"showPeerDASInfos" envconfig:"FRONTEND_SHOW_PEER_DAS_INFOS"` + ShowSubmitDeposit bool `yaml:"showSubmitDeposit" envconfig:"FRONTEND_SHOW_SUBMIT_DEPOSIT"` + ShowSubmitConsolidation bool `yaml:"showSubmitConsolidation" envconfig:"FRONTEND_SHOW_SUBMIT_CONSOLIDATION"` } `yaml:"frontend"` RateLimit struct { diff --git a/types/models/submit_consolidation.go b/types/models/submit_consolidation.go new file mode 100644 index 00000000..4a48a0c8 --- /dev/null +++ b/types/models/submit_consolidation.go @@ -0,0 +1,17 @@ +package models + +type SubmitConsolidationPageData struct { + NetworkName string `json:"netname"` + PublicRPCUrl string `json:"pubrpc"` + RainbowkitProjectId string `json:"rainbowkit"` + ChainId uint64 `json:"chainid"` + ConsolidationContract string `json:"consolidationcontract"` +} + +type SubmitConsolidationPageDataValidator struct { + Index uint64 `json:"index"` + Pubkey string `json:"pubkey"` + Balance uint64 `json:"balance"` + CredType string `json:"credtype"` + Status string `json:"status"` +} diff --git a/ui-package/package-lock.json b/ui-package/package-lock.json index 42c15f34..7338a3c5 100644 --- a/ui-package/package-lock.json +++ b/ui-package/package-lock.json @@ -18,6 +18,7 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.5", "react-dom": "^18.3.1", + "react-select": "^5.8.2", "viem": "^2.21.36", "wagmi": "^2.12.25" }, @@ -2386,12 +2387,135 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", + "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.2.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", + "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", "license": "MIT" }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.13.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", + "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/cache": "^11.13.0", + "@emotion/serialize": "^1.3.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", + "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@ethereumjs/common": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", @@ -2443,6 +2567,31 @@ "node": ">=14" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -4209,6 +4358,12 @@ "@types/node": "*" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -5423,6 +5578,80 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-plugin-macros/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -6918,7 +7147,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -7372,6 +7600,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", @@ -7777,6 +8011,15 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -8729,7 +8972,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-rpc-engine": { @@ -8848,7 +9090,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/listhen": { @@ -10130,7 +10371,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -10143,7 +10383,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10214,6 +10453,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -11207,6 +11455,33 @@ } } }, + "node_modules/react-select": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.2.tgz", + "integrity": "sha512-a/LkOckoI62710gGPQSQqUp7A10fGbH/ya3/IR49qaq3XoBvwymgD5mJgtiHxBDsutyEQfdKNycWVh8Cg8UCjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-select/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -12098,6 +12373,12 @@ "webpack": "^5.27.0" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/superstruct": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", @@ -12862,6 +13143,20 @@ } } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", @@ -13479,6 +13774,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/ui-package/package.json b/ui-package/package.json index f8ff2250..02a651bd 100644 --- a/ui-package/package.json +++ b/ui-package/package.json @@ -37,6 +37,7 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.5", "react-dom": "^18.3.1", + "react-select": "^5.8.2", "viem": "^2.21.36", "wagmi": "^2.12.25" }, diff --git a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx new file mode 100644 index 00000000..2c4a2af3 --- /dev/null +++ b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx @@ -0,0 +1,22 @@ +import React, { CSSProperties } from 'react'; +import { useAccount, useWriteContract } from 'wagmi'; +import { useState } from 'react'; +import { IValidator } from './SubmitConsolidationsFormProps'; + +interface IConsolidationReviewProps { + sourceValidator: IValidator; + targetValidator: IValidator; + consolidationContract: string; +} + +const ConsolidationReview = (props: IConsolidationReviewProps) => { + + return ( +
+ +
+ ); + +}; + +export default ConsolidationReview; diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss new file mode 100644 index 00000000..2b05c138 --- /dev/null +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss @@ -0,0 +1,24 @@ + +.validator-selector { + .validator-item { + display: flex; + } + + .validator-index { + width: 80px; + } + + .validator-pubkey { + width: 200px; + flex-grow: 1; + } + + .validator-balance { + padding-left: 10px; + width: 100px; + } + + .validator-status { + width: 60px; + } +} diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx new file mode 100644 index 00000000..d5ca7b13 --- /dev/null +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx @@ -0,0 +1,204 @@ +import React, { useEffect } from 'react'; +import { ConnectButton } from '@rainbow-me/rainbowkit'; +import { useAccount } from 'wagmi'; +import { useState } from 'react'; + +import { ISubmitConsolidationsFormProps, IValidator } from './SubmitConsolidationsFormProps'; +import './SubmitConsolidationsForm.scss'; +import ValidatorSelector, { formatBalance, formatStatus } from './ValidatorSelector'; +import ConsolidationReview from './ConsolidationReview'; + +const SubmitConsolidationsForm = (props: ISubmitConsolidationsFormProps): React.ReactElement => { + const { address: walletAddress, isConnected, chain } = useAccount(); + const [validators, setValidators] = useState(null); + const [loadingError, setLoadingError] = useState(null); + const [sourceValidator, setSourceValidator] = useState(null); + const [targetValidator, setTargetValidator] = useState(null); + + useEffect(() => { + if (walletAddress) { + props.loadValidatorsCallback(walletAddress).then(setValidators).catch(setLoadingError); + } else { + setValidators(null) + } + }, [walletAddress]); + + return ( +
+
+
+

Submit consolidation requests

+

This tool can be used to create consolidation requests for your validators.

+
+
+ +
+
+ Step 1: Connect your wallet +
+
+
+
+ +
+
+ + {isConnected && chain && validators == null ? +
+ Please wait while we load your validators... +
+ : null} + + {isConnected && chain && loadingError ? +
+ + Error loading validators: {loadingError.toString()} +
+ : null} + + {isConnected && chain && !loadingError && validators !== null ? + <> +
+
+ +
+
+
+ Select the validator you want to consolidate your funds from. This validator will be exited and its funds will be sent to the target validator. +
+
+
+ { + console.log("source validator", validator); + setSourceValidator(validator); + }} + value={sourceValidator} + /> +
+
+ + {sourceValidator ? +
+
+
+ Index: +
+
+ {sourceValidator.index} +
+
+
+
+ Pubkey: +
+
+ {sourceValidator.pubkey} +
+
+
+
+ Status: +
+
+ {formatStatus(sourceValidator.status)} +
+
+
+
+ Balance: +
+
+ {formatBalance(sourceValidator.balance, "ETH")} +
+
+
+ : null} + +
+
+ +
+
+
+ Select the validator you want to consolidate your funds to. This validator will receive the funds from the source validator and gets 0x02 withdrawal credentials assigned. +
+
+
+ { + console.log("target validator", validator); + setTargetValidator(validator); + }} + value={targetValidator} + /> +
+
+ {targetValidator ? +
+
+
+ Index: +
+
+ {targetValidator.index} +
+
+
+
+ Pubkey: +
+
+ {targetValidator.pubkey} +
+
+
+
+ Status: +
+
+ {formatStatus(targetValidator.status)} +
+
+
+
+ Balance: +
+
+ {formatBalance(targetValidator.balance, "ETH")} +
+
+
+ : null} + + + {sourceValidator && targetValidator ? + <> +
+
+ +
+
+ + + : null} + + : null} + +
+ ); +} + +export default SubmitConsolidationsForm; diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts new file mode 100644 index 00000000..6c269c4c --- /dev/null +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts @@ -0,0 +1,13 @@ + +export interface ISubmitConsolidationsFormProps { + consolidationContract: string; + loadValidatorsCallback: (address: string) => Promise; +} + +export interface IValidator { + index: number; + pubkey: string; + credtype: string; + balance: number; + status: string; +} diff --git a/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx b/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx new file mode 100644 index 00000000..aaa37f14 --- /dev/null +++ b/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx @@ -0,0 +1,85 @@ +import React, { CSSProperties } from 'react'; +import { useAccount, useWriteContract } from 'wagmi'; +import { useState } from 'react'; +import Select, { createFilter, OptionProps } from 'react-select' +import { IValidator } from './SubmitConsolidationsFormProps'; +import { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; + +interface IValidatorSelectorProps { + validators: IValidator[]; + onChange: (validator: IValidator) => void; + value: IValidator | null; +} + +const ValidatorSelector = (props: IValidatorSelectorProps): React.ReactElement => { + const filterConfig = { + ignoreCase: true, + ignoreAccents: true, + trim: true, + matchFrom: "any" as const, + stringify: (option: FilterOptionOption) => option.data.index + ": " + option.data.pubkey + }; + + return ( + + className="validator-selector" + options={props.validators} + placeholder="Select a validator" + components={{ + Option: ({ children, ...props }) => ( + + {children} + + ) + }} + onChange={(e) => { + props.onChange(e); + }} + filterOption={createFilter(filterConfig)} + isMulti={false} + isOptionSelected={(o, v) => v.some((i) => i.index === o.index)} + getOptionLabel={(o) => "Selected validator: [" + o.index + "] " + o.pubkey} + getOptionValue={(o) => o.pubkey} + value={props.value} + /> + ); + +} + +const ValidatorOption = (props: OptionProps) => { + const { data, getStyles, innerRef, innerProps } = props; + const styles = getStyles('option', props); + + return ( + + + {data.index} + {data.pubkey} + {formatBalance(data.balance, "ETH")} + {formatStatus(data.status)} + + + ); + +}; + +export function formatStatus(status: string) { + switch (status.toLowerCase()) { + case "active": + return {status}; + case "exited": + case "exiting": + case "slashed": + case "pending": + return {status}; + default: + return {status}; + } +} + +export function formatBalance(amount: number, ethSymbol: string) { + let amountEth = amount / 1e9; + return amountEth.toFixed(0) + " " + ethSymbol; +} + +export default ValidatorSelector; diff --git a/ui-package/src/main.tsx b/ui-package/src/main.tsx index 828fe5ee..14dc31a5 100644 --- a/ui-package/src/main.tsx +++ b/ui-package/src/main.tsx @@ -3,6 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { IWagmiRainbowProviderProps, IWagmiRainbowProviderConfig } from './components/WagmiRainbowProvider/WagmiRainbowProviderProps'; import { ISubmitDepositsFormProps } from './components/SubmitDepositsForm/SubmitDepositsFormProps'; +import { ISubmitConsolidationsFormProps } from './components/SubmitConsolidationsForm/SubmitConsolidationsFormProps'; export interface IComponentExports { [component: string]: (container: HTMLElement, cfg: any) => IComponentControls @@ -27,6 +28,18 @@ function exportComponents(uiPackages: IComponentExports) { ) } ); + + // SubmitConsolidationsForm component + const SubmitConsolidationsForm = React.lazy>(() => import(/* webpackChunkName: "submit-consolidation" */ './components/SubmitConsolidationsForm/SubmitConsolidationsForm')); + uiPackages.SubmitConsolidationsForm = buildComponentLoader<{wagmiConfig: IWagmiRainbowProviderConfig, submitConsolidationsConfig: ISubmitConsolidationsFormProps}>( + (config) => { + return ( + + + + ) + } + ); } function buildComponentLoader(loader: (cfg: TCfg) => React.ReactNode) { From 19215c32faa09320398e1554e9eb0c6ac49a881f Mon Sep 17 00:00:00 2001 From: pk910 Date: Thu, 31 Oct 2024 22:18:58 +0100 Subject: [PATCH 02/13] complete implementation of submit consolidations page --- handlers/submit_consolidation.go | 1 + .../submit_consolidation.html | 1 + types/models/submit_consolidation.go | 1 + .../ConsolidationReview.tsx | 194 +++++++++++++++++- .../SubmitConsolidationsForm.scss | 46 +++++ .../SubmitConsolidationsForm.tsx | 9 +- .../SubmitConsolidationsFormProps.ts | 1 + .../ValidatorSelector.tsx | 13 +- ui-package/src/utils/ReadableAmount.ts | 23 +++ 9 files changed, 279 insertions(+), 10 deletions(-) create mode 100644 ui-package/src/utils/ReadableAmount.ts diff --git a/handlers/submit_consolidation.go b/handlers/submit_consolidation.go index b37375dd..8f341602 100644 --- a/handlers/submit_consolidation.go +++ b/handlers/submit_consolidation.go @@ -93,6 +93,7 @@ func buildSubmitConsolidationPageData() (*models.SubmitConsolidationPageData, ti RainbowkitProjectId: utils.Config.Frontend.RainbowkitProjectId, ChainId: specs.DepositChainId, ConsolidationContract: execution.ConsolidationContractAddr, + ExplorerUrl: utils.Config.Frontend.EthExplorerLink, } return pageData, 1 * time.Hour diff --git a/templates/submit_consolidation/submit_consolidation.html b/templates/submit_consolidation/submit_consolidation.html index bddac115..a1fa3133 100644 --- a/templates/submit_consolidation/submit_consolidation.html +++ b/templates/submit_consolidation/submit_consolidation.html @@ -50,6 +50,7 @@

}, submitConsolidationsConfig: { consolidationContract: "{{ .ConsolidationContract }}", + explorerUrl: "{{ .ExplorerUrl }}", loadValidatorsCallback: function(address) { return fetch(`?ajax=load_validators&address=${address}`) .then(response => response.json()) diff --git a/types/models/submit_consolidation.go b/types/models/submit_consolidation.go index 4a48a0c8..f37fd1b0 100644 --- a/types/models/submit_consolidation.go +++ b/types/models/submit_consolidation.go @@ -6,6 +6,7 @@ type SubmitConsolidationPageData struct { RainbowkitProjectId string `json:"rainbowkit"` ChainId uint64 `json:"chainid"` ConsolidationContract string `json:"consolidationcontract"` + ExplorerUrl string `json:"explorerurl"` } type SubmitConsolidationPageDataValidator struct { diff --git a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx index 2c4a2af3..b242621f 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx @@ -1,22 +1,206 @@ -import React, { CSSProperties } from 'react'; -import { useAccount, useWriteContract } from 'wagmi'; +import React, { CSSProperties, useEffect } from 'react'; +import { useAccount, useReadContract, useSendTransaction, useWriteContract, usePrepareTransactionRequest } from 'wagmi'; +import { useCall } from 'wagmi' import { useState } from 'react'; import { IValidator } from './SubmitConsolidationsFormProps'; - +import { toReadableAmount } from '../../utils/ReadableAmount'; +import { Modal } from 'react-bootstrap'; interface IConsolidationReviewProps { sourceValidator: IValidator; targetValidator: IValidator; consolidationContract: string; + explorerUrl: string; } const ConsolidationReview = (props: IConsolidationReviewProps) => { - + const { address, chain } = useAccount(); + const [addExtraFee, setAddExtraFee] = useState(true); + const [errorModal, setErrorModal] = useState(null); + + const consolidationQueueLengthCall = useCall({ + account: address, + to: props.consolidationContract, + data: "0x", + chain: chain, + }); + const prepareRequest = usePrepareTransactionRequest(); + const submitRequest = useSendTransaction(); + + useEffect(() => { + const interval = setInterval(() => { + consolidationQueueLengthCall.refetch(); + }, 15000); + return () => { + clearInterval(interval); + }; + }, [consolidationQueueLengthCall]); + + let queueLength = 0n; + let isPreElectra = false; + let requiredFee = 0n; + let requestFee = 0n; + if (consolidationQueueLengthCall.isFetched && consolidationQueueLengthCall.data) { + var queueLenHex = consolidationQueueLengthCall.data.data as string; + if (queueLenHex == "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") { + isPreElectra = true; + } else { + queueLength = BigInt(queueLenHex); + requiredFee = getRequiredFee(queueLength); + + if(addExtraFee) { + requestFee = getRequiredFee(queueLength + 10n); // add extra fee for 10 consolidations submitted before this + } else { + requestFee = requiredFee; + } + } + } + + var feeFactor = 0; + var feeUnit = "Wei"; + + if (requestFee > 100000000000000n) { + feeFactor = 18; + feeUnit = "ETH"; + } else if (requestFee > 100000n) { + feeFactor = 9; + feeUnit = "Gwei"; + } + return (
- + {consolidationQueueLengthCall.isError ? +
+ Error loading queue length from consolidation contract.
+ {consolidationQueueLengthCall.error?.message}
+ +
+ : !consolidationQueueLengthCall.isFetched ? +

Loading...

+ : isPreElectra ? +
+ The network is not on Electra yet, so consolidation requests can not be submitted. +
+ :
+
+
+ Consolidation Contract: +
+
+ {props.consolidationContract} +
+
+
+
+ Consolidation Queue: +
+
+ {queueLength.toString()} Consolidations +
+
+
+
+ Required queue fee: +
+
+ {toReadableAmount(requiredFee, feeFactor, feeUnit, 4)} +
+
+
+
+ Add extra fee: +
+
+ setAddExtraFee(e.target.checked)} /> Add extra fee to avoid rejection due to other submissions +
+
+
+
+ Total fee: +
+
+ {toReadableAmount(requestFee, feeFactor, feeUnit, 4)} +
+
+
+
+ +
+
+ {submitRequest.isSuccess ? +
+
+
+ Consolidation TX: + {props.explorerUrl ? + {submitRequest.data} + : {submitRequest.data} + } +
+
+
+ : null} +
+ } + {errorModal && ( + setErrorModal(null)} size="lg"> + + Consolidation Transaction Failed + + +
{errorModal}
+
+ + + +
+ )}
); + function getRequiredFee(numerator: bigint): bigint { + let i = 1n; + let output = 0n; + let numeratorAccum = 1n * 17n; // factor * denominator + + while (numeratorAccum > 0n) { + output += numeratorAccum; + numeratorAccum = (numeratorAccum * numerator) / (17n * i); + i += 1n; + } + + return output / 17n; + } + + function submitConsolidation() { + submitRequest.sendTransactionAsync({ + to: props.consolidationContract, + account: address, + chainId: chain?.id, + value: requestFee, + data: "0x" + props.sourceValidator.pubkey.substring(2) + props.targetValidator.pubkey.substring(2), + }).then(tx => { + console.log(tx); + }).catch(error => { + setErrorModal(error.message); + }); + + } + }; export default ConsolidationReview; diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss index 2b05c138..7bd01f23 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss @@ -21,4 +21,50 @@ .validator-status { width: 60px; } + + + .validator-selector-control { + background-color: transparent; + } + + .validator-selector-menu { + background-color: var(--bs-body-bg); + } + + .validator-selector-option { + cursor: default; + display: block; + font-size: inherit; + width: 100%; + user-select: none; + -webkit-tap-highlight-color: rgba(128, 128, 128, 0.1); + background-color: transparent; + color: inherit; + padding: 8px 12px; + box-sizing: border-box; + } + + .validator-selector-option.selected { + color: var(--bs-body-color); + background-color: rgba(var(--bs-primary-rgb), 0.3); + } + + .validator-selector-option:hover { + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); + } + + .validator-selector-option.selected:hover { + color: var(--bs-body-color); + background-color: rgba(var(--bs-primary-rgb), 0.4); + } + + .validator-selector-single-value { + color: var(--bs-body-color); + } + + .validator-selector-input { + color: var(--bs-body-color); + } + } diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx index d5ca7b13..cde3b604 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx @@ -96,7 +96,9 @@ const SubmitConsolidationsForm = (props: ISubmitConsolidationsFormProps): React. Pubkey:
- {sourceValidator.pubkey} + + {sourceValidator.pubkey} +
@@ -155,7 +157,9 @@ const SubmitConsolidationsForm = (props: ISubmitConsolidationsFormProps): React. Pubkey:
- {targetValidator.pubkey} + + {targetValidator.pubkey} +
@@ -191,6 +195,7 @@ const SubmitConsolidationsForm = (props: ISubmitConsolidationsFormProps): React. sourceValidator={sourceValidator} targetValidator={targetValidator} consolidationContract={props.consolidationContract} + explorerUrl={props.explorerUrl} /> : null} diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts index 6c269c4c..27dc60f7 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsFormProps.ts @@ -1,6 +1,7 @@ export interface ISubmitConsolidationsFormProps { consolidationContract: string; + explorerUrl: string; loadValidatorsCallback: (address: string) => Promise; } diff --git a/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx b/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx index aaa37f14..2196cfa9 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx @@ -41,17 +41,24 @@ const ValidatorSelector = (props: IValidatorSelectorProps): React.ReactElement = getOptionLabel={(o) => "Selected validator: [" + o.index + "] " + o.pubkey} getOptionValue={(o) => o.pubkey} value={props.value} + classNames={{ + control: () => "validator-selector-control", + container: () => "validator-selector-container", + menu: () => "validator-selector-menu", + option: () => "validator-selector-option", + singleValue: () => "validator-selector-single-value", + input: () => "validator-selector-input" + }} /> ); } const ValidatorOption = (props: OptionProps) => { - const { data, getStyles, innerRef, innerProps } = props; - const styles = getStyles('option', props); + const { data, innerRef, innerProps, isSelected } = props; return ( - + {data.index} {data.pubkey} diff --git a/ui-package/src/utils/ReadableAmount.ts b/ui-package/src/utils/ReadableAmount.ts new file mode 100644 index 00000000..af99a562 --- /dev/null +++ b/ui-package/src/utils/ReadableAmount.ts @@ -0,0 +1,23 @@ + + +export function toDecimalUnit(amount: number, decimals?: number): number { + let factor = Math.pow(10, decimals || 18); + return amount / factor; +} + +export function toReadableAmount(amount: number | bigint, decimals?: number, unit?: string, precision?: number): string { + if(!decimals) + decimals = 18; + if(!precision) + precision = 3; + if(!amount) + return "0"+ (unit ? " " + unit : ""); + if(typeof amount === "bigint") + amount = Number(amount); + + let decimalAmount = toDecimalUnit(amount, decimals); + let precisionFactor = Math.pow(10, precision); + let amountStr = (Math.round(decimalAmount * precisionFactor) / precisionFactor).toString(); + + return amountStr + (unit ? " " + unit : ""); +} From 0b7e1f428917658750309df9439c6acf583b1fdf Mon Sep 17 00:00:00 2001 From: pk910 Date: Thu, 31 Oct 2024 22:45:31 +0100 Subject: [PATCH 03/13] filter consolidatable validators --- handlers/submit_consolidation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/submit_consolidation.go b/handlers/submit_consolidation.go index 8f341602..b366ae0b 100644 --- a/handlers/submit_consolidation.go +++ b/handlers/submit_consolidation.go @@ -111,11 +111,11 @@ func handleSubmitConsolidationPageDataAjax(w http.ResponseWriter, r *http.Reques validators := services.GlobalBeaconService.GetCachedValidatorSet() result := []models.SubmitConsolidationPageDataValidator{} for _, validator := range validators { - if validator.Validator.WithdrawalCredentials[0] == 0x00 && false { + if validator.Validator.WithdrawalCredentials[0] == 0x00 { continue } - if !bytes.Equal(validator.Validator.WithdrawalCredentials[12:], addressBytes[:]) && false { + if !bytes.Equal(validator.Validator.WithdrawalCredentials[12:], addressBytes[:]) { continue } From 9d9c3993480053fd70cb9c688d7941ed8a86326a Mon Sep 17 00:00:00 2001 From: pk910 Date: Thu, 31 Oct 2024 23:42:16 +0100 Subject: [PATCH 04/13] reset submit form when selecting different validators --- .../SubmitConsolidationsForm/SubmitConsolidationsForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx index cde3b604..5d8b7ee0 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx @@ -192,6 +192,7 @@ const SubmitConsolidationsForm = (props: ISubmitConsolidationsFormProps): React.
Date: Fri, 1 Nov 2024 00:04:11 +0100 Subject: [PATCH 05/13] cleanup & fixes for validator selection filter --- .../ConsolidationReview.tsx | 8 +++-- .../SubmitConsolidationsForm.scss | 4 +-- .../SubmitConsolidationsForm.tsx | 3 +- .../ValidatorSelector.tsx | 34 ++++++++++++------- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx index b242621f..6d3a8320 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx @@ -1,10 +1,11 @@ -import React, { CSSProperties, useEffect } from 'react'; -import { useAccount, useReadContract, useSendTransaction, useWriteContract, usePrepareTransactionRequest } from 'wagmi'; +import React, { useEffect } from 'react'; +import { useAccount, useSendTransaction, usePrepareTransactionRequest } from 'wagmi'; import { useCall } from 'wagmi' import { useState } from 'react'; import { IValidator } from './SubmitConsolidationsFormProps'; import { toReadableAmount } from '../../utils/ReadableAmount'; import { Modal } from 'react-bootstrap'; + interface IConsolidationReviewProps { sourceValidator: IValidator; targetValidator: IValidator; @@ -173,6 +174,7 @@ const ConsolidationReview = (props: IConsolidationReviewProps) => { ); function getRequiredFee(numerator: bigint): bigint { + // https://eips.ethereum.org/EIPS/eip-7251#fee-calculation let i = 1n; let output = 0n; let numeratorAccum = 1n * 17n; // factor * denominator @@ -192,6 +194,8 @@ const ConsolidationReview = (props: IConsolidationReviewProps) => { account: address, chainId: chain?.id, value: requestFee, + // https://eips.ethereum.org/EIPS/eip-7251#add-consolidation-request + // calldata (96 bytes): sourceValidator.pubkey (48 bytes) + targetValidator.pubkey (48 bytes) data: "0x" + props.sourceValidator.pubkey.substring(2) + props.targetValidator.pubkey.substring(2), }).then(tx => { console.log(tx); diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss index 7bd01f23..09b51f80 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.scss @@ -49,12 +49,12 @@ background-color: rgba(var(--bs-primary-rgb), 0.3); } - .validator-selector-option:hover { + .validator-selector-option:hover, .validator-selector-option.focused { color: var(--bs-body-color); background-color: var(--bs-tertiary-bg); } - .validator-selector-option.selected:hover { + .validator-selector-option.selected:hover, .validator-selector-option.selected.focused { color: var(--bs-body-color); background-color: rgba(var(--bs-primary-rgb), 0.4); } diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx index 5d8b7ee0..45ca449c 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx @@ -4,10 +4,11 @@ import { useAccount } from 'wagmi'; import { useState } from 'react'; import { ISubmitConsolidationsFormProps, IValidator } from './SubmitConsolidationsFormProps'; -import './SubmitConsolidationsForm.scss'; import ValidatorSelector, { formatBalance, formatStatus } from './ValidatorSelector'; import ConsolidationReview from './ConsolidationReview'; +import './SubmitConsolidationsForm.scss'; + const SubmitConsolidationsForm = (props: ISubmitConsolidationsFormProps): React.ReactElement => { const { address: walletAddress, isConnected, chain } = useAccount(); const [validators, setValidators] = useState(null); diff --git a/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx b/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx index 2196cfa9..86b3303f 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/ValidatorSelector.tsx @@ -1,6 +1,4 @@ -import React, { CSSProperties } from 'react'; -import { useAccount, useWriteContract } from 'wagmi'; -import { useState } from 'react'; +import React from 'react'; import Select, { createFilter, OptionProps } from 'react-select' import { IValidator } from './SubmitConsolidationsFormProps'; import { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; @@ -12,12 +10,16 @@ interface IValidatorSelectorProps { } const ValidatorSelector = (props: IValidatorSelectorProps): React.ReactElement => { - const filterConfig = { - ignoreCase: true, - ignoreAccents: true, - trim: true, - matchFrom: "any" as const, - stringify: (option: FilterOptionOption) => option.data.index + ": " + option.data.pubkey + const filterOptions = (option: FilterOptionOption, inputValue: string) => { + inputValue = inputValue.trim(); + if (inputValue) { + if(inputValue.startsWith("0x") || !/^[0-9]+$/.test(inputValue)) { + return option.data.pubkey.toLowerCase().includes(inputValue.toLowerCase()); + } else { + return option.data.index.toString().startsWith(inputValue); + } + } + return true; }; return ( @@ -35,7 +37,7 @@ const ValidatorSelector = (props: IValidatorSelectorProps): React.ReactElement = onChange={(e) => { props.onChange(e); }} - filterOption={createFilter(filterConfig)} + filterOption={filterOptions} isMulti={false} isOptionSelected={(o, v) => v.some((i) => i.index === o.index)} getOptionLabel={(o) => "Selected validator: [" + o.index + "] " + o.pubkey} @@ -55,10 +57,18 @@ const ValidatorSelector = (props: IValidatorSelectorProps): React.ReactElement = } const ValidatorOption = (props: OptionProps) => { - const { data, innerRef, innerProps, isSelected } = props; + const { data, innerRef, innerProps, isSelected, isFocused } = props; + let classNames = ["validator-selector-option"]; + if (isSelected) { + classNames.push("selected"); + } + if (isFocused) { + classNames.push("focused"); + } + return ( - + {data.index} {data.pubkey} From ed37df94d61a055ed4bed908220b8e8e9f30c306 Mon Sep 17 00:00:00 2001 From: pk910 Date: Fri, 1 Nov 2024 00:19:23 +0100 Subject: [PATCH 06/13] fix queue fee rendering --- ui-package/src/utils/ReadableAmount.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-package/src/utils/ReadableAmount.ts b/ui-package/src/utils/ReadableAmount.ts index af99a562..e65d8e0a 100644 --- a/ui-package/src/utils/ReadableAmount.ts +++ b/ui-package/src/utils/ReadableAmount.ts @@ -1,14 +1,14 @@ export function toDecimalUnit(amount: number, decimals?: number): number { - let factor = Math.pow(10, decimals || 18); + let factor = Math.pow(10, typeof decimals === "number" ? decimals : 18); return amount / factor; } export function toReadableAmount(amount: number | bigint, decimals?: number, unit?: string, precision?: number): string { - if(!decimals) + if(typeof decimals !== "number") decimals = 18; - if(!precision) + if(typeof precision !== "number") precision = 3; if(!amount) return "0"+ (unit ? " " + unit : ""); From 3df2bdcd4f6a4c954a551e095a3a1d0b1de238a5 Mon Sep 17 00:00:00 2001 From: pk910 Date: Fri, 1 Nov 2024 00:51:32 +0100 Subject: [PATCH 07/13] add label for extra fee checkbox --- .../SubmitConsolidationsForm/ConsolidationReview.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx index 6d3a8320..47a66d14 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx @@ -113,7 +113,8 @@ const ConsolidationReview = (props: IConsolidationReviewProps) => { Add extra fee:
- setAddExtraFee(e.target.checked)} /> Add extra fee to avoid rejection due to other submissions + setAddExtraFee(e.target.checked)} /> +
From 37b847d984f2d9f700eb6dd10e4db8aed32d214d Mon Sep 17 00:00:00 2001 From: pk910 Date: Fri, 1 Nov 2024 02:27:09 +0100 Subject: [PATCH 08/13] add submit withdrawals & exits page --- .hack/devnet/run.sh | 2 +- clients/consensus/chainspec.go | 1 + cmd/dora-explorer/main.go | 1 + config/default.config.yml | 2 +- handlers/pageData.go | 7 +- handlers/submit_consolidation.go | 6 +- handlers/submit_withdrawal.go | 161 ++++++++++ .../submit_withdrawal/submit_withdrawal.html | 70 +++++ test-config.yaml | 2 +- types/config.go | 8 +- types/models/submit_withdrawal.go | 19 ++ ui-package/Makefile | 3 + .../ConsolidationReview.tsx | 12 +- .../SubmitConsolidationsForm.tsx | 6 +- .../SubmitWithdrawalsForm.scss | 70 +++++ .../SubmitWithdrawalsForm.tsx | 276 ++++++++++++++++++ .../SubmitWithdrawalsFormProps.ts | 15 + .../ValidatorSelector.tsx | 102 +++++++ .../WithdrawalReview.tsx | 217 ++++++++++++++ ui-package/src/main.tsx | 14 +- ui-package/src/utils/ReadableAmount.ts | 7 +- 21 files changed, 984 insertions(+), 17 deletions(-) create mode 100644 handlers/submit_withdrawal.go create mode 100644 templates/submit_withdrawal/submit_withdrawal.html create mode 100644 types/models/submit_withdrawal.go create mode 100644 ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.scss create mode 100644 ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx create mode 100644 ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsFormProps.ts create mode 100644 ui-package/src/components/SubmitWithdrawalsForm/ValidatorSelector.tsx create mode 100644 ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx diff --git a/.hack/devnet/run.sh b/.hack/devnet/run.sh index 685675b3..477d107f 100755 --- a/.hack/devnet/run.sh +++ b/.hack/devnet/run.sh @@ -65,7 +65,7 @@ frontend: validatorNamesYaml: "${__dir}/generated-validator-ranges.yaml" showSensitivePeerInfos: true showSubmitDeposit: true - showSubmitConsolidation: true + showSubmitElRequests: true beaconapi: localCacheSize: 10 redisCacheAddr: "" diff --git a/clients/consensus/chainspec.go b/clients/consensus/chainspec.go index 8a8ee077..4e2c266a 100644 --- a/clients/consensus/chainspec.go +++ b/clients/consensus/chainspec.go @@ -53,6 +53,7 @@ type ChainSpec struct { MaxConsolidationRequestsPerPayload uint64 `yaml:"MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD"` MaxWithdrawalRequestsPerPayload uint64 `yaml:"MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD"` DepositChainId uint64 `yaml:"DEPOSIT_CHAIN_ID"` + MinActivationBalance uint64 `yaml:"MIN_ACTIVATION_BALANCE"` // EIP7594: PeerDAS NumberOfColumns *uint64 `yaml:"NUMBER_OF_COLUMNS"` diff --git a/cmd/dora-explorer/main.go b/cmd/dora-explorer/main.go index 568cac98..990e9ef4 100644 --- a/cmd/dora-explorer/main.go +++ b/cmd/dora-explorer/main.go @@ -167,6 +167,7 @@ func startFrontend(webserver *http.Server) { router.HandleFunc("/validators/el_withdrawals", handlers.ElWithdrawals).Methods("GET") router.HandleFunc("/validators/el_consolidations", handlers.ElConsolidations).Methods("GET") router.HandleFunc("/validators/submit_consolidations", handlers.SubmitConsolidation).Methods("GET") + router.HandleFunc("/validators/submit_withdrawals", handlers.SubmitWithdrawal).Methods("GET") router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET") router.HandleFunc("/validator/{index}/slots", handlers.ValidatorSlots).Methods("GET") diff --git a/config/default.config.yml b/config/default.config.yml index 4a950432..038705bd 100644 --- a/config/default.config.yml +++ b/config/default.config.yml @@ -35,7 +35,7 @@ frontend: showSensitivePeerInfos: false showPeerDASInfos: false showSubmitDeposit: false - showSubmitConsolidation: false + showSubmitElRequests: false beaconapi: # beacon node rpc endpoints diff --git a/handlers/pageData.go b/handlers/pageData.go index 5585dba5..6145982e 100644 --- a/handlers/pageData.go +++ b/handlers/pageData.go @@ -216,12 +216,17 @@ func createMenuItems(active string) []types.MainMenuItem { }) } - if utils.Config.Frontend.ShowSubmitConsolidation { + if utils.Config.Frontend.ShowSubmitElRequests { submitLinks = append(submitLinks, types.NavigationLink{ Label: "Submit Consolidations", Path: "/validators/submit_consolidations", Icon: "fa-square-plus", }) + submitLinks = append(submitLinks, types.NavigationLink{ + Label: "Submit Withdrawals & Exits", + Path: "/validators/submit_withdrawals", + Icon: "fa-money-bill-transfer", + }) } if len(submitLinks) > 0 { diff --git a/handlers/submit_consolidation.go b/handlers/submit_consolidation.go index b366ae0b..b88aadbf 100644 --- a/handlers/submit_consolidation.go +++ b/handlers/submit_consolidation.go @@ -27,8 +27,8 @@ func SubmitConsolidation(w http.ResponseWriter, r *http.Request) { ) var pageTemplate = templates.GetTemplate(submitConsolidationTemplateFiles...) - if !utils.Config.Frontend.ShowSubmitConsolidation { - handlePageError(w, r, errors.New("submit consolidation is not enabled")) + if !utils.Config.Frontend.ShowSubmitElRequests { + handlePageError(w, r, errors.New("submit el requests is not enabled")) return } @@ -140,7 +140,7 @@ func handleSubmitConsolidationPageDataAjax(w http.ResponseWriter, r *http.Reques Index: uint64(validator.Index), Pubkey: validator.Validator.PublicKey.String(), Balance: uint64(validator.Balance), - CredType: fmt.Sprintf("%x", validator.Validator.WithdrawalCredentials[0]), + CredType: fmt.Sprintf("%02x", validator.Validator.WithdrawalCredentials[0]), Status: status, }) } diff --git a/handlers/submit_withdrawal.go b/handlers/submit_withdrawal.go new file mode 100644 index 00000000..524a3a79 --- /dev/null +++ b/handlers/submit_withdrawal.go @@ -0,0 +1,161 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/ethereum/go-ethereum/common" + "github.com/sirupsen/logrus" + + "github.com/ethpandaops/dora/indexer/execution" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/templates" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" +) + +// SubmitWithdrawal will submit a withdrawal request +func SubmitWithdrawal(w http.ResponseWriter, r *http.Request) { + var submitWithdrawalTemplateFiles = append(layoutTemplateFiles, + "submit_withdrawal/submit_withdrawal.html", + ) + var pageTemplate = templates.GetTemplate(submitWithdrawalTemplateFiles...) + + if !utils.Config.Frontend.ShowSubmitElRequests { + handlePageError(w, r, errors.New("submit el requests is not enabled")) + return + } + + query := r.URL.Query() + if query.Has("ajax") { + err := handleSubmitWithdrawalPageDataAjax(w, r) + if err != nil { + handlePageError(w, r, err) + } + return + } + + pageData, pageError := getSubmitWithdrawalPageData() + if pageError != nil { + handlePageError(w, r, pageError) + return + } + if pageData == nil { + data := InitPageData(w, r, "blockchain", "/submit_withdrawal", "Submit Withdrawals & Exits", submitWithdrawalTemplateFiles) + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "submit_withdrawal.go", "Submit Withdrawals & Exits", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } + return + } + + data := InitPageData(w, r, "blockchain", "/submit_withdrawal", "Submit Withdrawals & Exits", submitWithdrawalTemplateFiles) + data.Data = pageData + w.Header().Set("Content-Type", "text/html") + if handleTemplateError(w, r, "submit_withdrawal.go", "Submit Withdrawals & Exits", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil { + return // an error has occurred and was processed + } +} + +func getSubmitWithdrawalPageData() (*models.SubmitWithdrawalPageData, error) { + pageData := &models.SubmitWithdrawalPageData{} + pageCacheKey := "submit_withdrawal" + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(pageCacheKey, true, pageData, func(pageCall *services.FrontendCacheProcessingPage) interface{} { + pageData, cacheTimeout := buildSubmitWithdrawalPageData() + pageCall.CacheTimeout = cacheTimeout + return pageData + }) + if pageErr == nil && pageRes != nil { + resData, resOk := pageRes.(*models.SubmitWithdrawalPageData) + if !resOk { + return nil, ErrInvalidPageModel + } + pageData = resData + } + return pageData, pageErr +} + +func buildSubmitWithdrawalPageData() (*models.SubmitWithdrawalPageData, time.Duration) { + logrus.Debugf("submit withdrawal page called") + + chainState := services.GlobalBeaconService.GetChainState() + specs := chainState.GetSpecs() + + pageData := &models.SubmitWithdrawalPageData{ + NetworkName: specs.ConfigName, + PublicRPCUrl: utils.Config.Frontend.PublicRPCUrl, + RainbowkitProjectId: utils.Config.Frontend.RainbowkitProjectId, + ChainId: specs.DepositChainId, + WithdrawalContract: execution.WithdrawalContractAddr, + ExplorerUrl: utils.Config.Frontend.EthExplorerLink, + MinValidatorBalance: specs.MinActivationBalance, + } + + return pageData, 1 * time.Hour +} + +func handleSubmitWithdrawalPageDataAjax(w http.ResponseWriter, r *http.Request) error { + query := r.URL.Query() + var pageData interface{} + + switch query.Get("ajax") { + case "load_validators": + address := query.Get("address") + addressBytes := common.HexToAddress(address) + + validators := services.GlobalBeaconService.GetCachedValidatorSet() + result := []models.SubmitWithdrawalPageDataValidator{} + for _, validator := range validators { + if validator.Validator.WithdrawalCredentials[0] == 0x00 { + continue + } + + if !bytes.Equal(validator.Validator.WithdrawalCredentials[12:], addressBytes[:]) { + continue + } + + var status string + if strings.HasPrefix(validator.Status.String(), "pending") { + status = "Pending" + } else if validator.Status == v1.ValidatorStateActiveOngoing { + status = "Active" + } else if validator.Status == v1.ValidatorStateActiveExiting { + status = "Exiting" + } else if validator.Status == v1.ValidatorStateActiveSlashed { + status = "Slashed" + } else if validator.Status == v1.ValidatorStateExitedUnslashed { + status = "Exited" + } else if validator.Status == v1.ValidatorStateExitedSlashed { + status = "Slashed" + } else { + status = validator.Status.String() + } + + result = append(result, models.SubmitWithdrawalPageDataValidator{ + Index: uint64(validator.Index), + Pubkey: validator.Validator.PublicKey.String(), + Balance: uint64(validator.Balance), + CredType: fmt.Sprintf("%02x", validator.Validator.WithdrawalCredentials[0]), + Status: status, + }) + } + + pageData = result + default: + return errors.New("invalid ajax request") + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(pageData) + if err != nil { + logrus.WithError(err).Error("error encoding index data") + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + } + return nil +} diff --git a/templates/submit_withdrawal/submit_withdrawal.html b/templates/submit_withdrawal/submit_withdrawal.html new file mode 100644 index 00000000..fb4d27e5 --- /dev/null +++ b/templates/submit_withdrawal/submit_withdrawal.html @@ -0,0 +1,70 @@ +{{ define "page" }} +
+
+

+ Submit Withdrawal/Exit Requests +

+ +
+ +
+ +
+
+ +
+
+
+
+ +{{ end }} +{{ define "js" }} + + +{{ end }} +{{ define "css" }} +{{ end }} \ No newline at end of file diff --git a/test-config.yaml b/test-config.yaml index 1ba6be99..117cabff 100644 --- a/test-config.yaml +++ b/test-config.yaml @@ -34,7 +34,7 @@ frontend: showSensitivePeerInfos: true showPeerDASInfos: true showSubmitDeposit: true - showSubmitConsolidation: true + showSubmitElRequests: true beaconapi: diff --git a/types/config.go b/types/config.go index 46bb2cf6..c9557033 100644 --- a/types/config.go +++ b/types/config.go @@ -51,10 +51,10 @@ type Config struct { HttpIdleTimeout time.Duration `yaml:"httpIdleTimeout" envconfig:"FRONTEND_HTTP_IDLE_TIMEOUT"` AllowDutyLoading bool `yaml:"allowDutyLoading" envconfig:"FRONTEND_ALLOW_DUTY_LOADING"` - ShowSensitivePeerInfos bool `yaml:"showSensitivePeerInfos" envconfig:"FRONTEND_SHOW_SENSITIVE_PEER_INFOS"` - ShowPeerDASInfos bool `yaml:"showPeerDASInfos" envconfig:"FRONTEND_SHOW_PEER_DAS_INFOS"` - ShowSubmitDeposit bool `yaml:"showSubmitDeposit" envconfig:"FRONTEND_SHOW_SUBMIT_DEPOSIT"` - ShowSubmitConsolidation bool `yaml:"showSubmitConsolidation" envconfig:"FRONTEND_SHOW_SUBMIT_CONSOLIDATION"` + ShowSensitivePeerInfos bool `yaml:"showSensitivePeerInfos" envconfig:"FRONTEND_SHOW_SENSITIVE_PEER_INFOS"` + ShowPeerDASInfos bool `yaml:"showPeerDASInfos" envconfig:"FRONTEND_SHOW_PEER_DAS_INFOS"` + ShowSubmitDeposit bool `yaml:"showSubmitDeposit" envconfig:"FRONTEND_SHOW_SUBMIT_DEPOSIT"` + ShowSubmitElRequests bool `yaml:"showSubmitElRequests" envconfig:"FRONTEND_SHOW_SUBMIT_EL_REQUESTS"` } `yaml:"frontend"` RateLimit struct { diff --git a/types/models/submit_withdrawal.go b/types/models/submit_withdrawal.go new file mode 100644 index 00000000..ee5241bb --- /dev/null +++ b/types/models/submit_withdrawal.go @@ -0,0 +1,19 @@ +package models + +type SubmitWithdrawalPageData struct { + NetworkName string `json:"netname"` + PublicRPCUrl string `json:"pubrpc"` + RainbowkitProjectId string `json:"rainbowkit"` + ChainId uint64 `json:"chainid"` + WithdrawalContract string `json:"withdrawalcontract"` + MinValidatorBalance uint64 `json:"minbalance"` + ExplorerUrl string `json:"explorerurl"` +} + +type SubmitWithdrawalPageDataValidator struct { + Index uint64 `json:"index"` + Pubkey string `json:"pubkey"` + Balance uint64 `json:"balance"` + CredType string `json:"credtype"` + Status string `json:"status"` +} diff --git a/ui-package/Makefile b/ui-package/Makefile index 31ea9665..16f89ab2 100644 --- a/ui-package/Makefile +++ b/ui-package/Makefile @@ -13,5 +13,8 @@ test: dep_npm build: dep_npm clean npm run build +debug-build: dep_npm clean + DEBUG=1 npm run build + clean: rm -rf dist/* diff --git a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx index 47a66d14..35945744 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { useAccount, useSendTransaction, usePrepareTransactionRequest } from 'wagmi'; +import { useAccount, useSendTransaction } from 'wagmi'; import { useCall } from 'wagmi' import { useState } from 'react'; import { IValidator } from './SubmitConsolidationsFormProps'; @@ -24,7 +24,6 @@ const ConsolidationReview = (props: IConsolidationReviewProps) => { data: "0x", chain: chain, }); - const prepareRequest = usePrepareTransactionRequest(); const submitRequest = useSendTransaction(); useEffect(() => { @@ -40,9 +39,12 @@ const ConsolidationReview = (props: IConsolidationReviewProps) => { let isPreElectra = false; let requiredFee = 0n; let requestFee = 0n; + let failedQueueLength = false; if (consolidationQueueLengthCall.isFetched && consolidationQueueLengthCall.data) { var queueLenHex = consolidationQueueLengthCall.data.data as string; - if (queueLenHex == "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") { + if (!queueLenHex) { + failedQueueLength = true; + } else if (queueLenHex == "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") { isPreElectra = true; } else { queueLength = BigInt(queueLenHex); @@ -77,6 +79,10 @@ const ConsolidationReview = (props: IConsolidationReviewProps) => { Retry
+ : failedQueueLength ? +
+ Error loading queue length from consolidation contract. (check contract address: {props.consolidationContract}) +
: !consolidationQueueLengthCall.isFetched ?

Loading...

: isPreElectra ? diff --git a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx index 45ca449c..e826eeba 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/SubmitConsolidationsForm.tsx @@ -40,7 +40,11 @@ const SubmitConsolidationsForm = (props: ISubmitConsolidationsFormProps): React.
- +
diff --git a/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.scss b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.scss new file mode 100644 index 00000000..09b51f80 --- /dev/null +++ b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.scss @@ -0,0 +1,70 @@ + +.validator-selector { + .validator-item { + display: flex; + } + + .validator-index { + width: 80px; + } + + .validator-pubkey { + width: 200px; + flex-grow: 1; + } + + .validator-balance { + padding-left: 10px; + width: 100px; + } + + .validator-status { + width: 60px; + } + + + .validator-selector-control { + background-color: transparent; + } + + .validator-selector-menu { + background-color: var(--bs-body-bg); + } + + .validator-selector-option { + cursor: default; + display: block; + font-size: inherit; + width: 100%; + user-select: none; + -webkit-tap-highlight-color: rgba(128, 128, 128, 0.1); + background-color: transparent; + color: inherit; + padding: 8px 12px; + box-sizing: border-box; + } + + .validator-selector-option.selected { + color: var(--bs-body-color); + background-color: rgba(var(--bs-primary-rgb), 0.3); + } + + .validator-selector-option:hover, .validator-selector-option.focused { + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); + } + + .validator-selector-option.selected:hover, .validator-selector-option.selected.focused { + color: var(--bs-body-color); + background-color: rgba(var(--bs-primary-rgb), 0.4); + } + + .validator-selector-single-value { + color: var(--bs-body-color); + } + + .validator-selector-input { + color: var(--bs-body-color); + } + +} diff --git a/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx new file mode 100644 index 00000000..b868a078 --- /dev/null +++ b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx @@ -0,0 +1,276 @@ +import React, { useEffect } from 'react'; +import { ConnectButton } from '@rainbow-me/rainbowkit'; +import { useAccount } from 'wagmi'; +import { useState } from 'react'; + +import { ISubmitWithdrawalsFormProps, IValidator } from './SubmitWithdrawalsFormProps'; +import ValidatorSelector, { formatBalance, formatStatus } from './ValidatorSelector'; +import WithdrawalReview from './WithdrawalReview'; + +import './SubmitWithdrawalsForm.scss'; +import { toReadableAmount } from '../../utils/ReadableAmount'; + +const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactElement => { + const { address: walletAddress, isConnected, chain } = useAccount(); + const [validators, setValidators] = useState(null); + const [loadingError, setLoadingError] = useState(null); + const [sourceValidator, setSourceValidator] = useState(null); + const [withdrawalType, setWithdrawalType] = useState(0); + const [withdrawalAmount, setWithdrawalAmount] = useState(0); + useEffect(() => { + if (walletAddress) { + props.loadValidatorsCallback(walletAddress).then(setValidators).catch(setLoadingError); + } else { + setValidators(null) + } + }, [walletAddress]); + + window.setTimeout(() => { + (window as any).explorer.initControls(); + }, 100); + + return ( +
+
+
+

Submit withdrawal requests

+

This tool can be used to create withdrawal requests for your validators.

+
+
+ +
+
+ Step 1: Connect your wallet +
+
+
+
+ +
+
+ + {isConnected && chain && validators == null ? +
+ Please wait while we load your validators... +
+ : null} + + {isConnected && chain && loadingError ? +
+ + Error loading validators: {loadingError.toString()} +
+ : null} + + {isConnected && chain && !loadingError && validators !== null ? + <> +
+
+ +
+
+
+ Select the validator you want to consolidate your funds from. This validator will be exited and its funds will be sent to the target validator. +
+
+
+ { + console.log("source validator", validator); + setSourceValidator(validator); + if(validator.credtype == "02") { + setWithdrawalType(0); + if(validator.balance > props.minValidatorBalance) { + setWithdrawalAmount(validator.balance - props.minValidatorBalance); + } else { + setWithdrawalAmount(1); + } + } else { + setWithdrawalType(1); + setWithdrawalAmount(1); + } + }} + value={sourceValidator} + /> +
+
+ + {sourceValidator ? +
+
+
+ Index: +
+
+ {sourceValidator.index} +
+
+
+
+ Pubkey: +
+ +
+
+
+ Status: +
+
+ {formatStatus(sourceValidator.status)} +
+
+
+
+ Balance: +
+
+ {toReadableAmount(sourceValidator.balance, 9, "ETH", 9)} +
+
+
+ : null} + + {sourceValidator ? + <> +
+
+ +
+
+
+ Select the amount you want to withdraw. You may also decide to withdraw all funds from the validator and exit it from the validator set. +
+
+
+
+ setWithdrawalType(0)} + /> + + {sourceValidator.credtype !== "02" ? + + + + : null} +
+
+
+
+ setWithdrawalType(1)} + /> + +
+
+
+ {withdrawalType == 0 ? + <> +
+
+ Validator Balance: +
+
+ {toReadableAmount(sourceValidator.balance, 9, "ETH", 9)} +
+
+
+
+ Withdrawable Balance: +
+
+ {toReadableAmount(sourceValidator.balance - props.minValidatorBalance, 9, "ETH", 9)} +
+
+
+
+ Requested Amount: +
+
+ setWithdrawalAmount(Math.floor(parseFloat(e.target.value) * 1000000000))} + /> +
+
+ ETH +
+
+
+ setWithdrawalAmount(parseInt(evt.target.value))} + value={withdrawalAmount} + /> +
+
+ + : null} + + : null} + + {sourceValidator && ((withdrawalType == 0 && withdrawalAmount > 0) || withdrawalType == 1) ? + <> +
+
+ +
+
+ + + : null} + + : null} + +
+ ); +} + +export default SubmitWithdrawalsForm; diff --git a/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsFormProps.ts b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsFormProps.ts new file mode 100644 index 00000000..305fad7b --- /dev/null +++ b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsFormProps.ts @@ -0,0 +1,15 @@ + +export interface ISubmitWithdrawalsFormProps { + withdrawalContract: string; + explorerUrl: string; + minValidatorBalance: number; + loadValidatorsCallback: (address: string) => Promise; +} + +export interface IValidator { + index: number; + pubkey: string; + credtype: string; + balance: number; + status: string; +} diff --git a/ui-package/src/components/SubmitWithdrawalsForm/ValidatorSelector.tsx b/ui-package/src/components/SubmitWithdrawalsForm/ValidatorSelector.tsx new file mode 100644 index 00000000..562b34f1 --- /dev/null +++ b/ui-package/src/components/SubmitWithdrawalsForm/ValidatorSelector.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import Select, { createFilter, OptionProps } from 'react-select' +import { IValidator } from './SubmitWithdrawalsFormProps'; +import { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; + +interface IValidatorSelectorProps { + validators: IValidator[]; + onChange: (validator: IValidator) => void; + value: IValidator | null; +} + +const ValidatorSelector = (props: IValidatorSelectorProps): React.ReactElement => { + const filterOptions = (option: FilterOptionOption, inputValue: string) => { + inputValue = inputValue.trim(); + if (inputValue) { + if(inputValue.startsWith("0x") || !/^[0-9]+$/.test(inputValue)) { + return option.data.pubkey.toLowerCase().includes(inputValue.toLowerCase()); + } else { + return option.data.index.toString().startsWith(inputValue); + } + } + return true; + }; + + return ( + + className="validator-selector" + options={props.validators} + placeholder="Select a validator" + components={{ + Option: ({ children, ...props }) => ( + + {children} + + ) + }} + onChange={(e) => { + props.onChange(e); + }} + filterOption={filterOptions} + isMulti={false} + isOptionSelected={(o, v) => v.some((i) => i.index === o.index)} + getOptionLabel={(o) => "Selected validator: [" + o.index + "] " + o.pubkey} + getOptionValue={(o) => o.pubkey} + value={props.value} + classNames={{ + control: () => "validator-selector-control", + container: () => "validator-selector-container", + menu: () => "validator-selector-menu", + option: () => "validator-selector-option", + singleValue: () => "validator-selector-single-value", + input: () => "validator-selector-input" + }} + /> + ); + +} + +const ValidatorOption = (props: OptionProps) => { + const { data, innerRef, innerProps, isSelected, isFocused } = props; + + let classNames = ["validator-selector-option"]; + if (isSelected) { + classNames.push("selected"); + } + if (isFocused) { + classNames.push("focused"); + } + + return ( + + + {data.index} + {data.pubkey} + {formatBalance(data.balance, "ETH")} + {formatStatus(data.status)} + + + ); + +}; + +export function formatStatus(status: string) { + switch (status.toLowerCase()) { + case "active": + return {status}; + case "exited": + case "exiting": + case "slashed": + case "pending": + return {status}; + default: + return {status}; + } +} + +export function formatBalance(amount: number, ethSymbol: string) { + let amountEth = amount / 1e9; + return amountEth.toFixed(0) + " " + ethSymbol; +} + +export default ValidatorSelector; diff --git a/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx b/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx new file mode 100644 index 00000000..5d1ddbde --- /dev/null +++ b/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx @@ -0,0 +1,217 @@ +import React, { useEffect } from 'react'; +import { useAccount, useSendTransaction } from 'wagmi'; +import { useCall } from 'wagmi' +import { useState } from 'react'; +import { IValidator } from './SubmitWithdrawalsFormProps'; +import { toReadableAmount } from '../../utils/ReadableAmount'; +import { Modal } from 'react-bootstrap'; + +interface IWithdrawalReviewProps { + sourceValidator: IValidator; + withdrawalAmount: number; + withdrawalContract: string; + explorerUrl: string; +} + +const WithdrawalReview = (props: IWithdrawalReviewProps) => { + const { address, chain } = useAccount(); + const [addExtraFee, setAddExtraFee] = useState(true); + const [errorModal, setErrorModal] = useState(null); + + const withdrawalQueueLengthCall = useCall({ + account: address, + to: props.withdrawalContract, + data: "0x", + chain: chain, + }); + const submitRequest = useSendTransaction(); + + useEffect(() => { + const interval = setInterval(() => { + withdrawalQueueLengthCall.refetch(); + }, 15000); + return () => { + clearInterval(interval); + }; + }, [withdrawalQueueLengthCall]); + + let queueLength = 0n; + let isPreElectra = false; + let requiredFee = 0n; + let requestFee = 0n; + let failedQueueLength = false; + if (withdrawalQueueLengthCall.isFetched && withdrawalQueueLengthCall.data) { + var queueLenHex = withdrawalQueueLengthCall.data.data as string; + if (!queueLenHex) { + failedQueueLength = true; + } else if (queueLenHex == "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") { + isPreElectra = true; + } else { + queueLength = BigInt(queueLenHex); + requiredFee = getRequiredFee(queueLength); + + if(addExtraFee) { + requestFee = getRequiredFee(queueLength + 10n); // add extra fee for 10 withdrawals submitted before this + } else { + requestFee = requiredFee; + } + } + } + + var feeFactor = 0; + var feeUnit = "Wei"; + + if (requestFee > 100000000000000n) { + feeFactor = 18; + feeUnit = "ETH"; + } else if (requestFee > 100000n) { + feeFactor = 9; + feeUnit = "Gwei"; + } + + return ( +
+ {withdrawalQueueLengthCall.isError ? +
+ Error loading queue length from withdrawal contract.
+ {withdrawalQueueLengthCall.error?.message}
+ +
+ : !withdrawalQueueLengthCall.isFetched ? +

Loading...

+ : failedQueueLength ? +
+ Error loading queue length from withdrawal contract. (check contract address: {props.withdrawalContract}) +
+ : isPreElectra ? +
+ The network is not on Electra yet, so withdrawal requests can not be submitted. +
+ :
+
+
+ Withdrawal Contract: +
+
+ {props.withdrawalContract} +
+
+
+
+ Withdrawal Queue: +
+
+ {queueLength.toString()} Withdrawals +
+
+
+
+ Required queue fee: +
+
+ {toReadableAmount(requiredFee, feeFactor, feeUnit, 4)} +
+
+
+
+ Add extra fee: +
+
+ setAddExtraFee(e.target.checked)} /> + +
+
+
+
+ Total fee: +
+
+ {toReadableAmount(requestFee, feeFactor, feeUnit, 4)} +
+
+
+
+ +
+
+ {submitRequest.isSuccess ? +
+
+
+ Withdrawal TX: + {props.explorerUrl ? + {submitRequest.data} + : {submitRequest.data} + } +
+
+
+ : null} +
+ } + {errorModal && ( + setErrorModal(null)} size="lg"> + + Withdrawal Transaction Failed + + +
{errorModal}
+
+ + + +
+ )} +
+ ); + + function getRequiredFee(numerator: bigint): bigint { + // https://eips.ethereum.org/EIPS/eip-7002#fee-calculation + let i = 1n; + let output = 0n; + let numeratorAccum = 1n * 17n; // factor * denominator + + while (numeratorAccum > 0n) { + output += numeratorAccum; + numeratorAccum = (numeratorAccum * numerator) / (17n * i); + i += 1n; + } + + return output / 17n; + } + + function submitWithdrawal() { + submitRequest.sendTransactionAsync({ + to: props.withdrawalContract, + account: address, + chainId: chain?.id, + value: requestFee, + // https://eips.ethereum.org/EIPS/eip-7002#add-withdrawal-request + // calldata (56 bytes): sourceValidator.pubkey (48 bytes) + amount (8 bytes) + data: "0x" + props.sourceValidator.pubkey.substring(2) + props.withdrawalAmount.toString(16).padStart(16, "0"), + }).then(tx => { + console.log(tx); + }).catch(error => { + setErrorModal(error.message); + }); + + } + +}; + +export default WithdrawalReview; diff --git a/ui-package/src/main.tsx b/ui-package/src/main.tsx index 14dc31a5..ff98198a 100644 --- a/ui-package/src/main.tsx +++ b/ui-package/src/main.tsx @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom/client'; import { IWagmiRainbowProviderProps, IWagmiRainbowProviderConfig } from './components/WagmiRainbowProvider/WagmiRainbowProviderProps'; import { ISubmitDepositsFormProps } from './components/SubmitDepositsForm/SubmitDepositsFormProps'; import { ISubmitConsolidationsFormProps } from './components/SubmitConsolidationsForm/SubmitConsolidationsFormProps'; - +import { ISubmitWithdrawalsFormProps } from './components/SubmitWithdrawalsForm/SubmitWithdrawalsFormProps'; export interface IComponentExports { [component: string]: (container: HTMLElement, cfg: any) => IComponentControls } @@ -40,6 +40,18 @@ function exportComponents(uiPackages: IComponentExports) { ) } ); + + // SubmitWithdrawalsForm component + const SubmitWithdrawalsForm = React.lazy>(() => import(/* webpackChunkName: "submit-withdrawal" */ './components/SubmitWithdrawalsForm/SubmitWithdrawalsForm')); + uiPackages.SubmitWithdrawalsForm = buildComponentLoader<{wagmiConfig: IWagmiRainbowProviderConfig, submitWithdrawalsConfig: ISubmitWithdrawalsFormProps}>( + (config) => { + return ( + + + + ) + } + ); } function buildComponentLoader(loader: (cfg: TCfg) => React.ReactNode) { diff --git a/ui-package/src/utils/ReadableAmount.ts b/ui-package/src/utils/ReadableAmount.ts index e65d8e0a..22734798 100644 --- a/ui-package/src/utils/ReadableAmount.ts +++ b/ui-package/src/utils/ReadableAmount.ts @@ -17,7 +17,12 @@ export function toReadableAmount(amount: number | bigint, decimals?: number, uni let decimalAmount = toDecimalUnit(amount, decimals); let precisionFactor = Math.pow(10, precision); - let amountStr = (Math.round(decimalAmount * precisionFactor) / precisionFactor).toString(); + let amountStr = (Math.round(decimalAmount * precisionFactor) / precisionFactor).toFixed(precision); + while (amountStr.endsWith("0")) { + amountStr = amountStr.slice(0, -1); + } + if(amountStr.endsWith(".")) + amountStr = amountStr.slice(0, -1); return amountStr + (unit ? " " + unit : ""); } From 034206dca8af07b19e433ff47fe6b18460776bdb Mon Sep 17 00:00:00 2001 From: pk910 Date: Fri, 1 Nov 2024 02:32:07 +0100 Subject: [PATCH 09/13] fix description --- .../components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx index b868a078..bf5407ec 100644 --- a/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx +++ b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx @@ -76,7 +76,7 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE
- Select the validator you want to consolidate your funds from. This validator will be exited and its funds will be sent to the target validator. + Select the validator you want to withdraw funds from or exit completely.
From 5f9044a13d77ad3e92e3b910d98132f7ccc8a93f Mon Sep 17 00:00:00 2001 From: pk910 Date: Fri, 1 Nov 2024 02:34:06 +0100 Subject: [PATCH 10/13] small fixes --- .../SubmitWithdrawalsForm.tsx | 39 +++++++++---------- .../WithdrawalReview.tsx | 4 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx index bf5407ec..4a6e3b1b 100644 --- a/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx +++ b/ui-package/src/components/SubmitWithdrawalsForm/SubmitWithdrawalsForm.tsx @@ -14,7 +14,7 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE const { address: walletAddress, isConnected, chain } = useAccount(); const [validators, setValidators] = useState(null); const [loadingError, setLoadingError] = useState(null); - const [sourceValidator, setSourceValidator] = useState(null); + const [validator, setValidator] = useState(null); const [withdrawalType, setWithdrawalType] = useState(0); const [withdrawalAmount, setWithdrawalAmount] = useState(0); useEffect(() => { @@ -71,20 +71,19 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE
- Select the validator you want to withdraw funds from or exit completely. + Select the validator you want to exit or withdraw funds from.
{ - console.log("source validator", validator); - setSourceValidator(validator); + setValidator(validator); if(validator.credtype == "02") { setWithdrawalType(0); if(validator.balance > props.minValidatorBalance) { @@ -97,19 +96,19 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE setWithdrawalAmount(1); } }} - value={sourceValidator} + value={validator} />
- {sourceValidator ? + {validator ?
Index:
- {sourceValidator.index} + {validator.index}
@@ -117,8 +116,8 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE Pubkey:
@@ -127,7 +126,7 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE Status:
- {formatStatus(sourceValidator.status)} + {formatStatus(validator.status)}
@@ -135,13 +134,13 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE Balance:
- {toReadableAmount(sourceValidator.balance, 9, "ETH", 9)} + {toReadableAmount(validator.balance, 9, "ETH", 9)}
: null} - {sourceValidator ? + {validator ? <>
@@ -167,7 +166,7 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE - {sourceValidator.credtype !== "02" ? + {validator.credtype !== "02" ?
- {toReadableAmount(sourceValidator.balance, 9, "ETH", 9)} + {toReadableAmount(validator.balance, 9, "ETH", 9)}
@@ -211,7 +210,7 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE Withdrawable Balance:
- {toReadableAmount(sourceValidator.balance - props.minValidatorBalance, 9, "ETH", 9)} + {toReadableAmount(validator.balance - props.minValidatorBalance, 9, "ETH", 9)}
@@ -237,7 +236,7 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE type="range" className="form-range" min={1} - max={sourceValidator.balance - props.minValidatorBalance} + max={validator.balance - props.minValidatorBalance} onChange={(evt) => setWithdrawalAmount(parseInt(evt.target.value))} value={withdrawalAmount} /> @@ -248,7 +247,7 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE : null} - {sourceValidator && ((withdrawalType == 0 && withdrawalAmount > 0) || withdrawalType == 1) ? + {validator && ((withdrawalType == 0 && withdrawalAmount > 0) || withdrawalType == 1) ? <>
@@ -258,8 +257,8 @@ const SubmitWithdrawalsForm = (props: ISubmitWithdrawalsFormProps): React.ReactE
{ value: requestFee, // https://eips.ethereum.org/EIPS/eip-7002#add-withdrawal-request // calldata (56 bytes): sourceValidator.pubkey (48 bytes) + amount (8 bytes) - data: "0x" + props.sourceValidator.pubkey.substring(2) + props.withdrawalAmount.toString(16).padStart(16, "0"), + data: "0x" + props.validator.pubkey.substring(2) + props.withdrawalAmount.toString(16).padStart(16, "0"), }).then(tx => { console.log(tx); }).catch(error => { From 9a171dfe159efb9649d41b139d84776cf6de764c Mon Sep 17 00:00:00 2001 From: pk910 Date: Fri, 1 Nov 2024 02:36:21 +0100 Subject: [PATCH 11/13] improve button text --- .../src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx b/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx index aab68dc6..d49f24e5 100644 --- a/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx +++ b/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx @@ -142,7 +142,7 @@ const WithdrawalReview = (props: IWithdrawalReviewProps) => { submitRequest.isError ? ( Retry withdrawal ) : ( - "Submit withdrawal" + "Submit " + (props.withdrawalAmount > 0 ? "withdrawal" : "exit") ) ) } From 8d976e52a9be23f4ac0e0c216a66ed7eb3d5b792 Mon Sep 17 00:00:00 2001 From: pk910 Date: Fri, 1 Nov 2024 02:38:24 +0100 Subject: [PATCH 12/13] change button text --- .../components/SubmitConsolidationsForm/ConsolidationReview.tsx | 2 +- .../src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx index 35945744..67a4d329 100644 --- a/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx +++ b/ui-package/src/components/SubmitConsolidationsForm/ConsolidationReview.tsx @@ -142,7 +142,7 @@ const ConsolidationReview = (props: IConsolidationReviewProps) => { submitRequest.isError ? ( Retry consolidation ) : ( - "Submit consolidation" + "Submit consolidation request" ) ) } diff --git a/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx b/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx index d49f24e5..6c376e35 100644 --- a/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx +++ b/ui-package/src/components/SubmitWithdrawalsForm/WithdrawalReview.tsx @@ -142,7 +142,7 @@ const WithdrawalReview = (props: IWithdrawalReviewProps) => { submitRequest.isError ? ( Retry withdrawal ) : ( - "Submit " + (props.withdrawalAmount > 0 ? "withdrawal" : "exit") + "Submit " + (props.withdrawalAmount > 0 ? "withdrawal" : "exit") + " request" ) ) } From e9825141e816a5020fd148a66c661fbae657b00e Mon Sep 17 00:00:00 2001 From: pk910 Date: Mon, 4 Nov 2024 14:04:32 +0100 Subject: [PATCH 13/13] trigger CI