From 2d193f8f4f7f3dc41d1b06aff36dec336ee29505 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 13 Feb 2025 14:41:20 +0700 Subject: [PATCH 1/3] feat: replace start timeout with hub startup state --- api/api.go | 1 + api/models.go | 1 + frontend/src/screens/Start.tsx | 60 +++++++++------------------------- frontend/src/types.ts | 1 + lnclient/ldk/ldk.go | 7 +++- service/models.go | 1 + service/service.go | 5 +++ service/start.go | 15 ++++++++- 8 files changed, 44 insertions(+), 47 deletions(-) diff --git a/api/api.go b/api/api.go index a94d3f0d..a966b556 100644 --- a/api/api.go +++ b/api/api.go @@ -718,6 +718,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { autoUnlockPassword, _ := api.cfg.Get("AutoUnlockPassword", "") info.SetupCompleted = api.cfg.SetupCompleted() info.Currency = api.cfg.GetCurrency() + info.StartupState = api.svc.GetStartupState() if api.startupError != nil { info.StartupError = api.startupError.Error() info.StartupErrorTime = api.startupErrorTime diff --git a/api/models.go b/api/models.go index c9e7a03d..cc2c9362 100644 --- a/api/models.go +++ b/api/models.go @@ -181,6 +181,7 @@ type InfoResponse struct { EnableAdvancedSetup bool `json:"enableAdvancedSetup"` LdkVssEnabled bool `json:"ldkVssEnabled"` VssSupported bool `json:"vssSupported"` + StartupState string `json:"startupState"` StartupError string `json:"startupError"` StartupErrorTime time.Time `json:"startupErrorTime"` AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"` diff --git a/frontend/src/screens/Start.tsx b/frontend/src/screens/Start.tsx index 72193ea7..48964e7c 100644 --- a/frontend/src/screens/Start.tsx +++ b/frontend/src/screens/Start.tsx @@ -12,26 +12,25 @@ import { AuthTokenResponse } from "src/types"; import { handleRequestError } from "src/utils/handleRequestError"; import { request } from "src/utils/request"; -const messages: string[] = [ - "Unlocking", - "Starting the wallet", - "Connecting to the network", - "Syncing", - "Still syncing, please wait...", -]; - export default function Start() { const [unlockPassword, setUnlockPassword] = React.useState(""); const [loading, setLoading] = React.useState(false); - const [buttonText, setButtonText] = React.useState("Login"); + const [buttonText, setButtonText] = React.useState("Start"); const { data: info } = useInfo(true); // poll the info endpoint to auto-redirect when app is running const { toast } = useToast(); + const startupState = info?.startupState; const startupError = info?.startupError; const startupErrorTime = info?.startupErrorTime; + React.useEffect(() => { + if (startupState) { + setButtonText(startupState); + } + }, [startupState]); + React.useEffect(() => { if (startupError && startupErrorTime) { toast({ @@ -45,45 +44,11 @@ export default function Start() { } }, [startupError, toast, startupErrorTime]); - React.useEffect(() => { - if (!loading) { - return; - } - let messageIndex = 1; - const intervalId = setInterval(() => { - if (messageIndex < messages.length) { - setButtonText(messages[messageIndex]); - messageIndex++; - } else { - clearInterval(intervalId); - } - }, 5000); - - const timeoutId = setTimeout(() => { - // if redirection didn't happen in 3 minutes info.running is false - toast({ - title: "Failed to start", - description: "Please try starting the node again.", - variant: "destructive", - }); - - setLoading(false); - setButtonText("Login"); - setUnlockPassword(""); - return; - }, 60000); // wait for 60 seconds - - return () => { - clearInterval(intervalId); - clearTimeout(timeoutId); - }; - }, [loading, toast]); - async function onSubmit(e: React.FormEvent) { e.preventDefault(); try { setLoading(true); - setButtonText(messages[0]); + setButtonText("Please wait..."); const authTokenResponse = await request("/api/start", { method: "POST", @@ -100,7 +65,7 @@ export default function Start() { } catch (error) { handleRequestError(toast, "Failed to connect", error); setLoading(false); - setButtonText("Login"); + setButtonText("Start"); setUnlockPassword(""); } } @@ -129,6 +94,11 @@ export default function Start() { {buttonText} + {loading && ( +

+ Starting Alby Hub may take a few minutes. +

+ )} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index bdceccc1..3cbe345d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -151,6 +151,7 @@ export interface InfoResponse { version: string; unlocked: boolean; enableAdvancedSetup: boolean; + startupState: string; startupError: string; startupErrorTime: string; autoUnlockPasswordSupported: boolean; diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index 306142d7..4203ce23 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -51,11 +51,13 @@ type LDKService struct { const resetRouterKey = "ResetRouter" -func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, network string, vssToken string) (result lnclient.LNClient, err error) { +func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, network string, vssToken string, setStartupState func(startupState string)) (result lnclient.LNClient, err error) { if mnemonic == "" || workDir == "" { return nil, errors.New("one or more required LDK configuration are missing") } + setStartupState("Configuring node") + // create dir if not exists newpath := filepath.Join(workDir) err = os.MkdirAll(newpath, os.ModePerm) @@ -152,6 +154,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events "vss_enabled": vssToken != "", "listening_addresses": listeningAddresses, }).Info("Creating node") + setStartupState("Loading node data...") var node *ldk_node.Node if vssToken != "" { node, err = builder.BuildWithVssStoreAndFixedHeaders(cfg.GetEnv().LDKVssUrl, "albyhub", map[string]string{ @@ -221,6 +224,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events } }() + setStartupState("Starting node...") err = node.Start() if err != nil { logger.Logger.WithError(err).Error("Failed to start LDK node") @@ -232,6 +236,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events "status": node.Status(), }).Info("Started LDK node. Syncing wallet...") + setStartupState("Syncing node...") syncStartTime := time.Now() err = node.SyncWallets() if err != nil { diff --git a/service/models.go b/service/models.go index c7e112d6..5bcebedc 100644 --- a/service/models.go +++ b/service/models.go @@ -25,4 +25,5 @@ type Service interface { GetConfig() config.Config GetKeys() keys.Keys IsRelayReady() bool + GetStartupState() string } diff --git a/service/service.go b/service/service.go index 14475f40..0c57e147 100644 --- a/service/service.go +++ b/service/service.go @@ -43,6 +43,7 @@ type service struct { appCancelFn context.CancelFunc keys keys.Keys isRelayReady atomic.Bool + startupState string } func NewService(ctx context.Context) (*service, error) { @@ -258,3 +259,7 @@ func (svc *service) setRelayReady(ready bool) { func (svc *service) IsRelayReady() bool { return svc.isRelayReady.Load() } + +func (svc *service) GetStartupState() string { + return svc.startupState +} diff --git a/service/start.go b/service/start.go index 108e3fc9..7694e780 100644 --- a/service/start.go +++ b/service/start.go @@ -222,6 +222,11 @@ func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscripti } func (svc *service) StartApp(encryptionKey string) error { + defer func() { + svc.startupState = "" + }() + + svc.startupState = "Initializing" albyIdentifier, err := svc.albyOAuthSvc.GetUserIdentifier() if err != nil { return err @@ -247,6 +252,7 @@ func (svc *service) StartApp(encryptionKey string) error { return err } + svc.startupState = "Launching Node" err = svc.launchLNBackend(ctx, encryptionKey) if err != nil { logger.Logger.Errorf("Failed to launch LN backend: %v", err) @@ -257,6 +263,7 @@ func (svc *service) StartApp(encryptionKey string) error { return err } + svc.startupState = "Connecting To Relay" err = svc.startNostr(ctx) if err != nil { cancelFn() @@ -307,7 +314,11 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e } vssEnabled = vssToken != "" - lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, mnemonic, ldkWorkdir, svc.cfg.GetEnv().LDKNetwork, vssToken) + svc.startupState = "Launching Node" + setStartupState := func(startupState string) { + svc.startupState = startupState + } + lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, mnemonic, ldkWorkdir, svc.cfg.GetEnv().LDKNetwork, vssToken, setStartupState) case config.GreenlightBackendType: Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey) GreenlightInviteCode, _ := svc.cfg.Get("GreenlightInviteCode", encryptionKey) @@ -395,6 +406,7 @@ func (svc *service) requestVssToken(ctx context.Context) (string, error) { // for brand new nodes, consider enabling VSS if nodeLastStartTime == "" && svc.cfg.GetEnv().LDKVssUrl != "" { + svc.startupState = "Checking Subscription" albyUserIdentifier, err := svc.albyOAuthSvc.GetUserIdentifier() if err != nil { logger.Logger.WithError(err).Error("Failed to fetch alby user identifier") @@ -416,6 +428,7 @@ func (svc *service) requestVssToken(ctx context.Context) (string, error) { vssToken := "" vssEnabled, _ := svc.cfg.Get("LdkVssEnabled", "") if vssEnabled == "true" { + svc.startupState = "Fetching VSS token" vssNodeIdentifier, err := ldk.GetVssNodeIdentifier(svc.keys) if err != nil { logger.Logger.WithError(err).Error("Failed to get VSS node identifier") From a1a0b5c3438ea41efa9f1193f3608269a910d158 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 13 Feb 2025 20:39:13 +0700 Subject: [PATCH 2/3] fix: update mocks --- README.md | 2 ++ tests/mocks/LNClient.go | 2 +- tests/mocks/Service.go | 47 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fa87e16e..73851109 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco We use [testify/mock](https://github.com/stretchr/testify) to facilitate mocking in tests. Instead of writing mocks manually, we generate them using [vektra/mockery](https://github.com/vektra/mockery). To regenerate them, [install mockery](https://vektra.github.io/mockery/latest/installation) and run it in the project's root directory: +> Use `go install github.com/vektra/mockery/v2@v2.52.1` as go 1.24.0 is currently not supported by Alby Hub. + $ mockery Mockery loads its configuration from the .mockery.yaml file in the root directory of this project. To add mocks for new interfaces, add them to the configuration file and run mockery. diff --git a/tests/mocks/LNClient.go b/tests/mocks/LNClient.go index fecf32ef..67e2a309 100644 --- a/tests/mocks/LNClient.go +++ b/tests/mocks/LNClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.52.1. DO NOT EDIT. package mocks diff --git a/tests/mocks/Service.go b/tests/mocks/Service.go index b5363f1f..466f5213 100644 --- a/tests/mocks/Service.go +++ b/tests/mocks/Service.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.52.1. DO NOT EDIT. package mocks @@ -314,6 +314,51 @@ func (_c *MockService_GetLNClient_Call) RunAndReturn(run func() lnclient.LNClien return _c } +// GetStartupState provides a mock function with no fields +func (_m *MockService) GetStartupState() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetStartupState") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockService_GetStartupState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStartupState' +type MockService_GetStartupState_Call struct { + *mock.Call +} + +// GetStartupState is a helper method to define mock.On call +func (_e *MockService_Expecter) GetStartupState() *MockService_GetStartupState_Call { + return &MockService_GetStartupState_Call{Call: _e.mock.On("GetStartupState")} +} + +func (_c *MockService_GetStartupState_Call) Run(run func()) *MockService_GetStartupState_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockService_GetStartupState_Call) Return(_a0 string) *MockService_GetStartupState_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockService_GetStartupState_Call) RunAndReturn(run func() string) *MockService_GetStartupState_Call { + _c.Call.Return(run) + return _c +} + // GetTransactionsService provides a mock function with no fields func (_m *MockService) GetTransactionsService() transactions.TransactionsService { ret := _m.Called() From e62d49b70897ee263ef4cd0729e70447986465e7 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 14 Feb 2025 16:03:21 +0700 Subject: [PATCH 3/3] fix: make start page copy consistent --- frontend/src/screens/Start.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/screens/Start.tsx b/frontend/src/screens/Start.tsx index 48964e7c..3774acd5 100644 --- a/frontend/src/screens/Start.tsx +++ b/frontend/src/screens/Start.tsx @@ -39,7 +39,7 @@ export default function Start() { variant: "destructive", }); setLoading(false); - setButtonText("Login"); + setButtonText("Start"); setUnlockPassword(""); } }, [startupError, toast, startupErrorTime]); @@ -75,7 +75,7 @@ export default function Start() {