Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Added a banner to remind user to backup their seed phrase when an account reaches a certain threshold.
- Gracefully shut down Electrum connections upon closing the app
- Show the selected coin's address on the confirmation screen
- Fetch balances of multiple ETH accounts at the same time, instead of one by one.

## v4.48.3
- Linux: fix compatiblity with some versions of Mesa also when using the AppImage
Expand Down
11 changes: 11 additions & 0 deletions backend/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,13 @@ func TestSortAccounts(t *testing.T) {
{Code: "acct-tbtc", CoinCode: coinpkg.CodeTBTC},
}
backend := newBackend(t, testnetDisabled, regtestDisabled)
unlockFN := backend.accountsAndKeystoreLock.Lock()
for i := range accountConfigs {
c, err := backend.Coin(accountConfigs[i].CoinCode)
require.NoError(t, err)
backend.createAndAddAccount(c, accountConfigs[i])
}
unlockFN()

expectedOrder := []accountsTypes.Code{
"acct-btc-1",
Expand Down Expand Up @@ -747,6 +749,7 @@ func TestCreateAndAddAccount(t *testing.T) {
// Add a Bitcoin account.
coin, err := b.Coin(coinpkg.CodeBTC)
require.NoError(t, err)
unlockFN := b.accountsAndKeystoreLock.Lock()
b.createAndAddAccount(
coin,
&config.Account{
Expand All @@ -757,6 +760,7 @@ func TestCreateAndAddAccount(t *testing.T) {
},
},
)
unlockFN()
require.Len(t, b.Accounts(), 1)
// Check some properties of the newly added account.
acct := b.Accounts()[0]
Expand All @@ -767,6 +771,8 @@ func TestCreateAndAddAccount(t *testing.T) {
// Add a Litecoin account.
coin, err = b.Coin(coinpkg.CodeLTC)
require.NoError(t, err)

unlockFN = b.accountsAndKeystoreLock.Lock()
b.createAndAddAccount(coin,
&config.Account{
Code: "test-ltc-account-code",
Expand All @@ -776,6 +782,7 @@ func TestCreateAndAddAccount(t *testing.T) {
},
},
)
unlockFN()
require.Len(t, b.Accounts(), 2)
// Check some properties of the newly added account.
acct = b.Accounts()[1]
Expand All @@ -786,6 +793,7 @@ func TestCreateAndAddAccount(t *testing.T) {
// Add an Ethereum account with some active ERC20 tokens.
coin, err = b.Coin(coinpkg.CodeETH)
require.NoError(t, err)
unlockFN = b.accountsAndKeystoreLock.Lock()
b.createAndAddAccount(coin,
&config.Account{
Code: "test-eth-account-code",
Expand All @@ -796,6 +804,7 @@ func TestCreateAndAddAccount(t *testing.T) {
ActiveTokens: []string{"eth-erc20-mkr"},
},
)
unlockFN()
// 2 more accounts: the added ETH account plus the active token for the ETH account.
require.Len(t, b.Accounts(), 4)
// Check some properties of the newly added account.
Expand All @@ -812,6 +821,7 @@ func TestCreateAndAddAccount(t *testing.T) {
// Add another Ethereum account with some active ERC20 tokens.
coin, err = b.Coin(coinpkg.CodeETH)
require.NoError(t, err)
unlockFN = b.accountsAndKeystoreLock.Lock()
b.createAndAddAccount(coin,
&config.Account{
Code: "test-eth-account-code-2",
Expand All @@ -823,6 +833,7 @@ func TestCreateAndAddAccount(t *testing.T) {
ActiveTokens: []string{"eth-erc20-usdt", "eth-erc20-bat"},
},
)
unlockFN()
// 3 more accounts: the added ETH account plus the two active tokens for the ETH account.
require.Len(t, b.Accounts(), 7)
// Check some properties of the newly added accounts.
Expand Down
116 changes: 99 additions & 17 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/ethereum/go-ethereum/params"
"github.com/sirupsen/logrus"
"golang.org/x/time/rate"
)

func init() {
Expand Down Expand Up @@ -225,10 +226,11 @@ type Backend struct {

socksProxy socksproxy.SocksProxy
// can be a regular or, if Tor is enabled in the config, a SOCKS5 proxy client.
httpClient *http.Client
etherScanHTTPClient *http.Client
ratesUpdater *rates.RateUpdater
banners *banners.Banners
httpClient *http.Client
etherScanHTTPClient *http.Client
etherScanRateLimiter *rate.Limiter
ratesUpdater *rates.RateUpdater
banners *banners.Banners

// For unit tests, called when `backend.checkAccountUsed()` is called.
tstCheckAccountUsed func(accounts.Interface) bool
Expand All @@ -240,6 +242,15 @@ type Backend struct {

// isOnline indicates whether the backend is online, i.e. able to connect to the internet.
isOnline atomic.Bool

// quit is used to indicate to running goroutines that they should stop as the backend is being closed
quit chan struct{}

// enqueueUpdateForAccount is used to enqueue an update for an account.
enqueueUpdateForAccount chan *eth.Account

// updateETHAccountsCh is used to trigger an update of all ETH accounts.
updateETHAccountsCh chan struct{}
Comment on lines +246 to +253
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned somewhere that we could wrap this into a dedicated struct and move all related methods there to pollute the backend less with ETH specifics. ethUpdater *ETHUpdater for example. Then unit tests could be added too more easily. Let's do this after this is merged?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah can do of course in a next PR to keep the scope smaller. I

}

// NewBackend creates a new backend with the given arguments.
Expand All @@ -262,11 +273,13 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
return nil, err
}

accountUpdate := make(chan *eth.Account)

backend := &Backend{
arguments: arguments,
environment: environment,
config: backendConfig,
events: make(chan interface{}, 1000),
events: make(chan interface{}),

devices: map[string]device.Interface{},
coins: map[coinpkg.Code]coinpkg.Coin{},
Expand All @@ -276,12 +289,15 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
return btc.NewAccount(config, coin, gapLimits, getAddress, log, hclient)
},
makeEthAccount: func(config *accounts.AccountConfig, coin *eth.Coin, httpClient *http.Client, log *logrus.Entry) accounts.Interface {
return eth.NewAccount(config, coin, httpClient, log)
return eth.NewAccount(config, coin, httpClient, log, accountUpdate)
},

log: log,

testing: backendConfig.AppConfig().Backend.StartInTestnet || arguments.Testing(),
testing: backendConfig.AppConfig().Backend.StartInTestnet || arguments.Testing(),
quit: make(chan struct{}),
etherScanRateLimiter: rate.NewLimiter(rate.Limit(etherscan.CallsPerSec), 1),
enqueueUpdateForAccount: accountUpdate,
}
// TODO: remove when connectivity check is present on all platforms
backend.isOnline.Store(true)
Expand Down Expand Up @@ -529,19 +545,19 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) {
coin = btc.NewCoin(coinpkg.CodeLTC, "Litecoin", "LTC", coinpkg.BtcUnitDefault, &ltc.MainNetParams, dbFolder, servers,
"https://blockchair.com/litecoin/transaction/", backend.socksProxy)
case code == coinpkg.CodeETH:
etherScan := etherscan.NewEtherScan("1", backend.etherScanHTTPClient)
etherScan := etherscan.NewEtherScan("1", backend.etherScanHTTPClient, backend.etherScanRateLimiter)
coin = eth.NewCoin(etherScan, code, "Ethereum", "ETH", "ETH", params.MainnetChainConfig,
"https://etherscan.io/tx/",
etherScan,
nil)
case code == coinpkg.CodeSEPETH:
etherScan := etherscan.NewEtherScan("11155111", backend.etherScanHTTPClient)
etherScan := etherscan.NewEtherScan("11155111", backend.etherScanHTTPClient, backend.etherScanRateLimiter)
coin = eth.NewCoin(etherScan, code, "Ethereum Sepolia", "SEPETH", "SEPETH", params.SepoliaChainConfig,
"https://sepolia.etherscan.io/tx/",
etherScan,
nil)
case erc20Token != nil:
etherScan := etherscan.NewEtherScan("1", backend.etherScanHTTPClient)
etherScan := etherscan.NewEtherScan("1", backend.etherScanHTTPClient, backend.etherScanRateLimiter)
coin = eth.NewCoin(etherScan, erc20Token.code, erc20Token.name, erc20Token.unit, "ETH", params.MainnetChainConfig,
"https://etherscan.io/tx/",
etherScan,
Expand All @@ -555,6 +571,74 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) {
return coin, nil
}

func (backend *Backend) pollETHAccounts() {
Comment thread
benma marked this conversation as resolved.
timer := time.After(0)

updateAll := func() {
if err := backend.updateETHAccounts(); err != nil {
backend.log.WithError(err).Error("could not update ETH accounts")
}
}

for {
select {
case <-backend.quit:
Comment thread
benma marked this conversation as resolved.
return
default:
select {
case <-backend.quit:
return
case account := <-backend.enqueueUpdateForAccount:
Comment thread
benma marked this conversation as resolved.
go func() {
// A single ETH accounts needs an update.
ethCoin, ok := account.Coin().(*eth.Coin)
if !ok {
backend.log.WithField("account", account.Config().Config.Name).Errorf("expected ETH account to have ETH coin, got %T", account.Coin())
}
etherScanClient := etherscan.NewEtherScan(ethCoin.ChainIDstr(), backend.etherScanHTTPClient, backend.etherScanRateLimiter)
if err := eth.UpdateBalances([]*eth.Account{account}, etherScanClient); err != nil {
backend.log.WithError(err).Errorf("could not update account %s", account.Config().Config.Name)
}
}()
case <-backend.updateETHAccountsCh:
go updateAll()
timer = time.After(eth.PollInterval)
case <-timer:
go updateAll()
timer = time.After(eth.PollInterval)
}
}
}
}

func (backend *Backend) updateETHAccounts() error {
defer backend.accountsAndKeystoreLock.RLock()()
backend.log.Debug("Updating ETH accounts balances")

accountsChainID := map[string][]*eth.Account{}
for _, account := range backend.accounts {
ethAccount, ok := account.(*eth.Account)
if ok {
ethCoin, ok := ethAccount.Coin().(*eth.Coin)
if !ok {
return errp.Newf("expected ETH account to have ETH coin, got %T", ethAccount.Coin())
}
chainID := ethCoin.ChainIDstr()
accountsChainID[chainID] = append(accountsChainID[chainID], ethAccount)
}

}

for chainID, ethAccounts := range accountsChainID {
etherScanClient := etherscan.NewEtherScan(chainID, backend.etherScanHTTPClient, backend.etherScanRateLimiter)
if err := eth.UpdateBalances(ethAccounts, etherScanClient); err != nil {
backend.log.WithError(err).Errorf("could not update ETH accounts for chain ID %s", chainID)
}
}

return 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.
Expand Down Expand Up @@ -592,13 +676,7 @@ func (backend *Backend) ManualReconnect(reconnectETH bool) {
}
if reconnectETH {
backend.log.Info("Reconnecting ETH accounts")
for _, account := range backend.accounts {
ethAccount, ok := account.(*eth.Account)
if !ok {
continue
}
ethAccount.EnqueueUpdate()
}
backend.updateETHAccountsCh <- struct{}{}
}
}

Expand Down Expand Up @@ -661,6 +739,8 @@ func (backend *Backend) Start() <-chan interface{} {

backend.environment.OnAuthSettingChanged(backend.config.AppConfig().Backend.Authentication)

go backend.pollETHAccounts()

if backend.config.AppConfig().Backend.StartInTestnet {
if err := backend.config.ModifyAppConfig(func(c *config.AppConfig) error { c.Backend.StartInTestnet = false; return nil }); err != nil {
backend.log.WithError(err).Error("Can't set StartInTestnet to false")
Expand Down Expand Up @@ -933,6 +1013,8 @@ func (backend *Backend) Close() error {
if len(errors) > 0 {
return errp.New(strings.Join(errors, "; "))
}

close(backend.quit)
return nil
}

Expand Down
Loading