From b52d3aca2be4a08f6d414e63ecafda1601d3061d Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 30 Sep 2024 11:26:58 +0200 Subject: [PATCH] backend: add ManualReconnect method and call on iOS When the iOS app goes into the background, its TCP connections (Electrum server connetions) are killed after a while (~30s). When the user goes back into the app, we want to immediatelly trigger a reconnect, not waiting for the regular 30s reconnect timer. ``` go get github.com/BitBoxSwiss/block-client-go@924dde9 go mod tidy go mod vendor ``` --- backend/backend.go | 37 +++++++++ backend/bridgecommon/bridgecommon.go | 12 +++ backend/coins/btc/blockchain/blockchain.go | 1 + .../coins/btc/blockchain/mocks/Interface.go | 81 ++++++++++++++++--- backend/coins/btc/blockchain/mocks/mock.go | 8 ++ backend/coins/btc/electrum/failoverclient.go | 4 + backend/mobileserver/mobileserver.go | 7 +- .../BitBoxApp/BitBoxApp/BitBoxAppApp.swift | 12 +-- go.mod | 2 +- go.sum | 4 +- .../block-client-go/failover/failover.go | 25 +++++- vendor/modules.txt | 2 +- 12 files changed, 171 insertions(+), 24 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 36fc3ad82a..4e1ce4293b 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -538,6 +538,43 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) { return coin, nil } +// ManualReconnect triggers reconnecting to Electrum servers if their connection is down. +// Only coin connections that were previously established are reconnected. +// Calling this is a no-op for coins that are already connected. +func (backend *Backend) ManualReconnect() { + var electrumCoinCodes []coinpkg.Code + if backend.arguments.Testing() { + electrumCoinCodes = []coinpkg.Code{ + coinpkg.CodeTBTC, + coinpkg.CodeTLTC, + } + } else { + electrumCoinCodes = []coinpkg.Code{ + coinpkg.CodeBTC, + coinpkg.CodeLTC, + } + } + backend.log.Info("Manually reconnecting") + for _, code := range electrumCoinCodes { + c, err := backend.Coin(code) + if err != nil { + backend.log.WithError(err).Errorf("could not find coin: %s", code) + continue + } + btcCoin, ok := c.(*btc.Coin) + if !ok { + backend.log.Errorf("Expected %s to be a btc coin", code) + continue + } + blockchain := btcCoin.Blockchain() + if blockchain == nil { + // Not initialized yet + continue + } + blockchain.ManualReconnect() + } +} + // Testing returns whether this backend is for testing only. func (backend *Backend) Testing() bool { return backend.arguments.Testing() diff --git a/backend/bridgecommon/bridgecommon.go b/backend/bridgecommon/bridgecommon.go index 86d71dc6ef..7d4e306724 100644 --- a/backend/bridgecommon/bridgecommon.go +++ b/backend/bridgecommon/bridgecommon.go @@ -166,6 +166,18 @@ func UsingMobileDataChanged() { }) } +// ManualReconnect exposes the ManualReconnect backend method. +func ManualReconnect() { + mu.RLock() + defer mu.RUnlock() + + if globalBackend == nil { + return + } + globalBackend.ManualReconnect() + +} + // BackendEnvironment implements backend.Environment. type BackendEnvironment struct { NotifyUserFunc func(string) diff --git a/backend/coins/btc/blockchain/blockchain.go b/backend/coins/btc/blockchain/blockchain.go index 249ed2d6e0..eb74ae0085 100644 --- a/backend/coins/btc/blockchain/blockchain.go +++ b/backend/coins/btc/blockchain/blockchain.go @@ -118,4 +118,5 @@ type Interface interface { Close() ConnectionError() error RegisterOnConnectionErrorChangedEvent(func(error)) + ManualReconnect() } diff --git a/backend/coins/btc/blockchain/mocks/Interface.go b/backend/coins/btc/blockchain/mocks/Interface.go index 112d23293b..e0b0dd11bd 100644 --- a/backend/coins/btc/blockchain/mocks/Interface.go +++ b/backend/coins/btc/blockchain/mocks/Interface.go @@ -1,17 +1,15 @@ -// Code generated by mockery v2.12.2. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package mocks import ( - btcutil "github.com/btcsuite/btcd/btcutil" blockchain "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/blockchain" + btcutil "github.com/btcsuite/btcd/btcutil" chainhash "github.com/btcsuite/btcd/chaincfg/chainhash" mock "github.com/stretchr/testify/mock" - testing "testing" - types "github.com/BitBoxSwiss/block-client-go/electrum/types" wire "github.com/btcsuite/btcd/wire" @@ -31,6 +29,10 @@ func (_m *Interface) Close() { func (_m *Interface) ConnectionError() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for ConnectionError") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -45,14 +47,21 @@ func (_m *Interface) ConnectionError() error { func (_m *Interface) EstimateFee(_a0 int) (btcutil.Amount, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for EstimateFee") + } + var r0 btcutil.Amount + var r1 error + if rf, ok := ret.Get(0).(func(int) (btcutil.Amount, error)); ok { + return rf(_a0) + } if rf, ok := ret.Get(0).(func(int) btcutil.Amount); ok { r0 = rf(_a0) } else { r0 = ret.Get(0).(btcutil.Amount) } - var r1 error if rf, ok := ret.Get(1).(func(int) error); ok { r1 = rf(_a0) } else { @@ -66,7 +75,15 @@ func (_m *Interface) EstimateFee(_a0 int) (btcutil.Amount, error) { func (_m *Interface) GetMerkle(_a0 chainhash.Hash, _a1 int) (*blockchain.GetMerkleResult, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetMerkle") + } + var r0 *blockchain.GetMerkleResult + var r1 error + if rf, ok := ret.Get(0).(func(chainhash.Hash, int) (*blockchain.GetMerkleResult, error)); ok { + return rf(_a0, _a1) + } if rf, ok := ret.Get(0).(func(chainhash.Hash, int) *blockchain.GetMerkleResult); ok { r0 = rf(_a0, _a1) } else { @@ -75,7 +92,6 @@ func (_m *Interface) GetMerkle(_a0 chainhash.Hash, _a1 int) (*blockchain.GetMerk } } - var r1 error if rf, ok := ret.Get(1).(func(chainhash.Hash, int) error); ok { r1 = rf(_a0, _a1) } else { @@ -89,7 +105,15 @@ func (_m *Interface) GetMerkle(_a0 chainhash.Hash, _a1 int) (*blockchain.GetMerk func (_m *Interface) Headers(_a0 int, _a1 int) (*blockchain.HeadersResult, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for Headers") + } + var r0 *blockchain.HeadersResult + var r1 error + if rf, ok := ret.Get(0).(func(int, int) (*blockchain.HeadersResult, error)); ok { + return rf(_a0, _a1) + } if rf, ok := ret.Get(0).(func(int, int) *blockchain.HeadersResult); ok { r0 = rf(_a0, _a1) } else { @@ -98,7 +122,6 @@ func (_m *Interface) Headers(_a0 int, _a1 int) (*blockchain.HeadersResult, error } } - var r1 error if rf, ok := ret.Get(1).(func(int, int) error); ok { r1 = rf(_a0, _a1) } else { @@ -113,6 +136,11 @@ func (_m *Interface) HeadersSubscribe(_a0 func(*types.Header)) { _m.Called(_a0) } +// ManualReconnect provides a mock function with given fields: +func (_m *Interface) ManualReconnect() { + _m.Called() +} + // RegisterOnConnectionErrorChangedEvent provides a mock function with given fields: _a0 func (_m *Interface) RegisterOnConnectionErrorChangedEvent(_a0 func(error)) { _m.Called(_a0) @@ -122,14 +150,21 @@ func (_m *Interface) RegisterOnConnectionErrorChangedEvent(_a0 func(error)) { func (_m *Interface) RelayFee() (btcutil.Amount, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for RelayFee") + } + var r0 btcutil.Amount + var r1 error + if rf, ok := ret.Get(0).(func() (btcutil.Amount, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() btcutil.Amount); ok { r0 = rf() } else { r0 = ret.Get(0).(btcutil.Amount) } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -143,7 +178,15 @@ func (_m *Interface) RelayFee() (btcutil.Amount, error) { func (_m *Interface) ScriptHashGetHistory(_a0 blockchain.ScriptHashHex) (blockchain.TxHistory, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for ScriptHashGetHistory") + } + var r0 blockchain.TxHistory + var r1 error + if rf, ok := ret.Get(0).(func(blockchain.ScriptHashHex) (blockchain.TxHistory, error)); ok { + return rf(_a0) + } if rf, ok := ret.Get(0).(func(blockchain.ScriptHashHex) blockchain.TxHistory); ok { r0 = rf(_a0) } else { @@ -152,7 +195,6 @@ func (_m *Interface) ScriptHashGetHistory(_a0 blockchain.ScriptHashHex) (blockch } } - var r1 error if rf, ok := ret.Get(1).(func(blockchain.ScriptHashHex) error); ok { r1 = rf(_a0) } else { @@ -171,6 +213,10 @@ func (_m *Interface) ScriptHashSubscribe(_a0 func() func(), _a1 blockchain.Scrip func (_m *Interface) TransactionBroadcast(_a0 *wire.MsgTx) error { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for TransactionBroadcast") + } + var r0 error if rf, ok := ret.Get(0).(func(*wire.MsgTx) error); ok { r0 = rf(_a0) @@ -185,7 +231,15 @@ func (_m *Interface) TransactionBroadcast(_a0 *wire.MsgTx) error { func (_m *Interface) TransactionGet(_a0 chainhash.Hash) (*wire.MsgTx, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for TransactionGet") + } + var r0 *wire.MsgTx + var r1 error + if rf, ok := ret.Get(0).(func(chainhash.Hash) (*wire.MsgTx, error)); ok { + return rf(_a0) + } if rf, ok := ret.Get(0).(func(chainhash.Hash) *wire.MsgTx); ok { r0 = rf(_a0) } else { @@ -194,7 +248,6 @@ func (_m *Interface) TransactionGet(_a0 chainhash.Hash) (*wire.MsgTx, error) { } } - var r1 error if rf, ok := ret.Get(1).(func(chainhash.Hash) error); ok { r1 = rf(_a0) } else { @@ -204,8 +257,12 @@ func (_m *Interface) TransactionGet(_a0 chainhash.Hash) (*wire.MsgTx, error) { return r0, r1 } -// NewInterface creates a new instance of Interface. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewInterface(t testing.TB) *Interface { +// NewInterface creates a new instance of Interface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *Interface { mock := &Interface{} mock.Mock.Test(t) diff --git a/backend/coins/btc/blockchain/mocks/mock.go b/backend/coins/btc/blockchain/mocks/mock.go index bff6b1c4d0..db76814734 100644 --- a/backend/coins/btc/blockchain/mocks/mock.go +++ b/backend/coins/btc/blockchain/mocks/mock.go @@ -39,6 +39,7 @@ type BlockchainMock struct { MockConnectionError func() error MockRegisterOnConnectionErrorChangedEvent func(func(error)) + MockManualReconnect func() } // ScriptHashGetHistory implements Interface. @@ -135,3 +136,10 @@ func (b *BlockchainMock) RegisterOnConnectionErrorChangedEvent(f func(error)) { b.MockRegisterOnConnectionErrorChangedEvent(f) } } + +// ManualReconnect implements Interface. +func (b *BlockchainMock) ManualReconnect() { + if b.MockManualReconnect != nil { + b.MockManualReconnect() + } +} diff --git a/backend/coins/btc/electrum/failoverclient.go b/backend/coins/btc/electrum/failoverclient.go index a443e785e1..ef104be651 100644 --- a/backend/coins/btc/electrum/failoverclient.go +++ b/backend/coins/btc/electrum/failoverclient.go @@ -156,6 +156,10 @@ func (f *failoverClient) TransactionGet(txHash chainhash.Hash) (*wire.MsgTx, err }) } +func (f *failoverClient) ManualReconnect() { + f.failover.ManualReconnect() +} + func (f *failoverClient) Close() { f.failover.Close() } diff --git a/backend/mobileserver/mobileserver.go b/backend/mobileserver/mobileserver.go index 57b21bf8de..9128048045 100644 --- a/backend/mobileserver/mobileserver.go +++ b/backend/mobileserver/mobileserver.go @@ -227,8 +227,13 @@ func CancelAuth() { bridgecommon.CancelAuth() } -// AuthResult triggers an auth feeedback notification (auth-ok/auth-err) towards the frontend, +// AuthResult triggers an auth feedback notification (auth-ok/auth-err) towards the frontend, // depending on the input value. func AuthResult(ok bool) { bridgecommon.AuthResult(ok) } + +// ManualReconnect wraps bridgecommon.ManualReconnect. +func ManualReconnect() { + bridgecommon.ManualReconnect() +} diff --git a/frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift b/frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift index d488825dcc..248fada3e2 100644 --- a/frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift +++ b/frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift @@ -91,7 +91,7 @@ class GoEnvironment: NSObject, MobileserverGoEnvironmentInterfaceProtocol { class GoAPI: NSObject, MobileserverGoAPIInterfaceProtocol, SetMessageHandlersProtocol { var handlers: MessageHandlersProtocol? - + func pushNotify(_ msg: String?) { self.handlers?.pushNotificationHandler(msg: msg!) } @@ -99,16 +99,15 @@ class GoAPI: NSObject, MobileserverGoAPIInterfaceProtocol, SetMessageHandlersPro func respond(_ queryID: Int, response: String?) { self.handlers?.callResponseHandler(queryID: queryID, response: response!) } - + func setMessageHandlers(handlers: MessageHandlersProtocol) { self.handlers = handlers } } - @main struct BitBoxAppApp: App { - var body: some Scene { + var body: some Scene { WindowGroup { GridLayout(alignment: .leading) { let goAPI = GoAPI() @@ -118,12 +117,13 @@ struct BitBoxAppApp: App { setupGoAPI(goAPI: goAPI) } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in - MobileserverTriggerAuth() + MobileserverManualReconnect() + MobileserverTriggerAuth() } } } } - + func setupGoAPI(goAPI: MobileserverGoAPIInterfaceProtocol) { let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! do { diff --git a/go.mod b/go.mod index 8e87a92ed3..430be73a8d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/BitBoxSwiss/bitbox02-api-go v0.0.0-20240925080402-a2115fee878e - github.com/BitBoxSwiss/block-client-go v0.0.0-20240516081043-0d604acd6519 + github.com/BitBoxSwiss/block-client-go v0.0.0-20241009081439-924dde98b9c1 github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcutil v1.1.6 diff --git a/go.sum b/go.sum index e3fdd62af6..aed142ec86 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/BitBoxSwiss/bitbox02-api-go v0.0.0-20240925080402-a2115fee878e h1:wEIIFhiZ58RsVjoKfwSBBD0i75uZ7KATOI/elaBWOOY= github.com/BitBoxSwiss/bitbox02-api-go v0.0.0-20240925080402-a2115fee878e/go.mod h1:Spf6hQRSylrvdjd7Cv4Tc8rFwlcamJAC8EuA61ARy7U= -github.com/BitBoxSwiss/block-client-go v0.0.0-20240516081043-0d604acd6519 h1:diVA/i8TJFBl9ZyMNX15KjZBpI2Gu63xQTozu6FsTrA= -github.com/BitBoxSwiss/block-client-go v0.0.0-20240516081043-0d604acd6519/go.mod h1:SJTiQZU9ggBzVKMni97rpNS9GddPKErndFXNSDrfEGc= +github.com/BitBoxSwiss/block-client-go v0.0.0-20241009081439-924dde98b9c1 h1:5hjP8mYSVKFibesrz8L6U0Vp5zSJt0LwXB3DSZGhnSo= +github.com/BitBoxSwiss/block-client-go v0.0.0-20241009081439-924dde98b9c1/go.mod h1:SJTiQZU9ggBzVKMni97rpNS9GddPKErndFXNSDrfEGc= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= diff --git a/vendor/github.com/BitBoxSwiss/block-client-go/failover/failover.go b/vendor/github.com/BitBoxSwiss/block-client-go/failover/failover.go index c076714279..cd937b394e 100644 --- a/vendor/github.com/BitBoxSwiss/block-client-go/failover/failover.go +++ b/vendor/github.com/BitBoxSwiss/block-client-go/failover/failover.go @@ -121,6 +121,8 @@ type Failover[C Client] struct { enableRetry bool subscriptions []func(client C, currentClientCounter int) + manualReconnect chan struct{} + closed bool closedMu sync.RWMutex } @@ -145,6 +147,7 @@ func New[C Client](opts *Options[C]) *Failover[C] { opts: opts, startServerIndex: startServerIndex, currentServerIndex: startServerIndex, + manualReconnect: make(chan struct{}), } } @@ -157,7 +160,17 @@ func (f *Failover[C]) establishConnection() error { if f.opts.OnRetry != nil { go f.opts.OnRetry(f.lastErr) } - time.Sleep(retryTimeout) + + // Drain the manualReconnect channel to avoid stale reconnect signals + select { + case <-f.manualReconnect: + default: + } + // Wait for retry timeout or manual reconnect + select { + case <-time.After(retryTimeout): + case <-f.manualReconnect: + } } f.enableRetry = true @@ -370,6 +383,16 @@ func (f *Failover[C]) isClosed() bool { return f.closed } +// ManualReconnect triggers a manual reconnect, non-blocking. +// This re-tries connecting immediately without waiting for the retry timeout. +// We we are not currently disconnected, this is a no-op. +func (f *Failover[C]) ManualReconnect() { + select { + case f.manualReconnect <- struct{}{}: + default: + } +} + // Close closes the failover client and closes the current client, resulting in `ErrClosed` in all // future `Call` and `Subscribe` calls. It also calls `Close()` on the currently active client if // one exists. diff --git a/vendor/modules.txt b/vendor/modules.txt index 4143777c63..fcd989efaf 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -8,7 +8,7 @@ github.com/BitBoxSwiss/bitbox02-api-go/communication/u2fhid github.com/BitBoxSwiss/bitbox02-api-go/util/errp github.com/BitBoxSwiss/bitbox02-api-go/util/semver github.com/BitBoxSwiss/bitbox02-api-go/util/sleep -# github.com/BitBoxSwiss/block-client-go v0.0.0-20240516081043-0d604acd6519 +# github.com/BitBoxSwiss/block-client-go v0.0.0-20241009081439-924dde98b9c1 ## explicit; go 1.19 github.com/BitBoxSwiss/block-client-go/electrum github.com/BitBoxSwiss/block-client-go/electrum/types