Skip to content

Commit b8e9e30

Browse files
committed
Merge branch 'grouped' into staging-watchonly
2 parents 321ec56 + 18aea1d commit b8e9e30

File tree

7 files changed

+128
-40
lines changed

7 files changed

+128
-40
lines changed

backend/config/accounts.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,25 @@ func (cfg AccountsConfig) Lookup(code accountsTypes.Code) *Account {
104104
return nil
105105
}
106106

107-
// GetOrAddKeystore looks up the keystore by root fingerprint. If it does not exist, one is added to
108-
// the list of keystores and the newly created one is returned.
109-
func (cfg *AccountsConfig) GetOrAddKeystore(rootFingerprint []byte) *Keystore {
107+
// LookupKeystore looks up a keystore by fingerprint. Returns error if it could not be found.
108+
func (cfg AccountsConfig) LookupKeystore(rootFingerprint []byte) (*Keystore, error) {
110109
for _, ks := range cfg.Keystores {
111110
if bytes.Equal(ks.RootFingerprint, rootFingerprint) {
112-
return ks
111+
return ks, nil
113112
}
114113
}
114+
return nil, errp.Newf("could not retrieve keystore for fingerprint %x", rootFingerprint)
115+
}
116+
117+
// GetOrAddKeystore looks up the keystore by root fingerprint. If it does not exist, one is added to
118+
// the list of keystores and the newly created one is returned.
119+
func (cfg *AccountsConfig) GetOrAddKeystore(rootFingerprint []byte) *Keystore {
120+
ks, err := cfg.LookupKeystore(rootFingerprint)
121+
if err == nil {
122+
return ks
123+
}
115124

116-
ks := &Keystore{RootFingerprint: rootFingerprint}
125+
ks = &Keystore{RootFingerprint: rootFingerprint}
117126
cfg.Keystores = append(cfg.Keystores, ks)
118127
return ks
119128
}

backend/handlers/handlers.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,10 @@ type activeToken struct {
341341
}
342342

343343
type accountJSON struct {
344+
// Multiple accounts can belong to the same keystore. For now we replicate the keystore info in
345+
// the accounts. In the future the getAccountsHandler() could return the accounts grouped
346+
// keystore.
347+
Keystore config.Keystore `json:"keystore"`
344348
Active bool `json:"active"`
345349
CoinCode coinpkg.Code `json:"coinCode"`
346350
CoinUnit string `json:"coinUnit"`
@@ -352,10 +356,11 @@ type accountJSON struct {
352356
BlockExplorerTxPrefix string `json:"blockExplorerTxPrefix"`
353357
}
354358

355-
func newAccountJSON(account accounts.Interface, activeTokens []activeToken) *accountJSON {
359+
func newAccountJSON(keystore config.Keystore, account accounts.Interface, activeTokens []activeToken) *accountJSON {
356360
eth, ok := account.Coin().(*eth.Coin)
357361
isToken := ok && eth.ERC20Token() != nil
358362
return &accountJSON{
363+
Keystore: keystore,
359364
Active: !account.Config().Config.Inactive,
360365
CoinCode: account.Coin().Code(),
361366
CoinUnit: account.Coin().Unit(false),
@@ -515,27 +520,42 @@ func (handlers *Handlers) getKeystoresHandler(_ *http.Request) interface{} {
515520
}
516521

517522
func (handlers *Handlers) getAccountsHandler(_ *http.Request) interface{} {
518-
accounts := []*accountJSON{}
519523
persistedAccounts := handlers.backend.Config().AccountsConfig()
524+
525+
accounts := []*accountJSON{}
520526
for _, account := range handlers.backend.Accounts() {
521527
if account.Config().Config.HiddenBecauseUnused {
522528
continue
523529
}
524530
var activeTokens []activeToken
531+
532+
persistedAccount := persistedAccounts.Lookup(account.Config().Config.Code)
533+
if persistedAccount == nil {
534+
handlers.log.WithField("code", account.Config().Config.Code).Error("account not found in accounts database")
535+
continue
536+
}
537+
525538
if account.Coin().Code() == coinpkg.CodeETH {
526-
persistedAccount := persistedAccounts.Lookup(account.Config().Config.Code)
527-
if persistedAccount == nil {
528-
handlers.log.WithField("code", account.Config().Config.Code).Error("account not found in accounts database")
529-
continue
530-
}
531539
for _, tokenCode := range persistedAccount.ActiveTokens {
532540
activeTokens = append(activeTokens, activeToken{
533541
TokenCode: tokenCode,
534542
AccountCode: backend.Erc20AccountCode(account.Config().Config.Code, tokenCode),
535543
})
536544
}
537545
}
538-
accounts = append(accounts, newAccountJSON(account, activeTokens))
546+
547+
rootFingerprint, err := persistedAccount.SigningConfigurations.RootFingerprint()
548+
if err != nil {
549+
handlers.log.WithField("code", account.Config().Config.Code).Error("could not identify root fingerprint")
550+
continue
551+
}
552+
keystore, err := persistedAccounts.LookupKeystore(rootFingerprint)
553+
if err != nil {
554+
handlers.log.WithField("code", account.Config().Config.Code).Error("could not find keystore of account")
555+
continue
556+
}
557+
558+
accounts = append(accounts, newAccountJSON(*keystore, account, activeTokens))
539559
}
540560
return accounts
541561
}

backend/signing/configuration.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,25 @@ func (configuration *Configuration) String() string {
221221
return fmt.Sprintf("ethereumSimple;%s", configuration.EthereumSimple.KeyInfo)
222222
}
223223

224-
// Configurations is an unordered collection of configurations.
224+
// Configurations is an unordered collection of configurations. All entries must have the same root
225+
// fingerprint.
225226
type Configurations []*Configuration
226227

228+
// RootFingerprint gets the fingerprint of the first config (assuming that all configurations have
229+
// the same rootFingerprint). Returns an error if the list has no entries or does not contain a
230+
// known config.
231+
func (configs Configurations) RootFingerprint() ([]byte, error) {
232+
for _, config := range configs {
233+
if config.BitcoinSimple != nil {
234+
return config.BitcoinSimple.KeyInfo.RootFingerprint, nil
235+
}
236+
if config.EthereumSimple != nil {
237+
return config.EthereumSimple.KeyInfo.RootFingerprint, nil
238+
}
239+
}
240+
return nil, errp.New("Could not retrieve fingerprint from signing configurations")
241+
}
242+
227243
// ContainsRootFingerprint returns true if the rootFingerprint is present in one of the configurations.
228244
func (configs Configurations) ContainsRootFingerprint(rootFingerprint []byte) bool {
229245
for _, config := range configs {

frontends/web/src/api/account.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,26 @@ export type Terc20Token = {
3636
};
3737

3838
export interface IActiveToken {
39-
tokenCode: string;
40-
accountCode: AccountCode;
39+
tokenCode: string;
40+
accountCode: AccountCode;
4141
}
4242

43+
export type TKeystore = {
44+
rootFingerprint: string;
45+
name: string;
46+
};
47+
4348
export interface IAccount {
44-
active: boolean;
45-
coinCode: CoinCode;
46-
coinUnit: string;
47-
coinName: string;
48-
code: AccountCode;
49-
name: string;
50-
isToken: boolean;
51-
activeTokens?: IActiveToken[];
52-
blockExplorerTxPrefix: string;
49+
keystore: TKeystore;
50+
active: boolean;
51+
coinCode: CoinCode;
52+
coinUnit: string;
53+
coinName: string;
54+
code: AccountCode;
55+
name: string;
56+
isToken: boolean;
57+
activeTokens?: IActiveToken[];
58+
blockExplorerTxPrefix: string;
5359
}
5460

5561
export const getAccounts = (): Promise<IAccount[]> => {

frontends/web/src/components/sidebar/sidebar.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { apiPost } from '../../utils/request';
3131
import Logo, { AppLogoInverted } from '../icon/logo';
3232
import { useLocation } from 'react-router';
3333
import { CloseXWhite } from '../icon';
34-
import { isBitcoinOnly } from '../../routes/account/utils';
34+
import { getAccountsByKeystore, isBitcoinOnly } from '../../routes/account/utils';
3535
import { SkipForTesting } from '../../routes/device/components/skipfortesting';
3636
import { Store } from '../../decorators/store';
3737
import style from './sidebar.module.css';
@@ -169,6 +169,7 @@ class Sidebar extends Component<Props> {
169169
} = this.props;
170170
const hidden = sidebarStatus === 'forceHidden';
171171
const hasOnlyBTCAccounts = accounts.every(({ coinCode }) => isBitcoinOnly(coinCode));
172+
const accountsByKeystore = getAccountsByKeystore(accounts);
172173
return (
173174
<div className={[style.sidebarContainer, hidden ? style.forceHide : ''].join(' ')}>
174175
<div className={[style.sidebarOverlay, activeSidebar ? style.active : ''].join(' ')} onClick={toggleSidebar}></div>
@@ -184,11 +185,6 @@ class Sidebar extends Component<Props> {
184185
</button>
185186
</div>
186187

187-
<div className={style.sidebarHeaderContainer}>
188-
<span className={style.sidebarHeader} hidden={!keystores?.length}>
189-
{t('sidebar.accounts')}
190-
</span>
191-
</div>
192188
{ accounts.length ? (
193189
<div className={style.sidebarItem}>
194190
<NavLink
@@ -203,7 +199,19 @@ class Sidebar extends Component<Props> {
203199
</NavLink>
204200
</div>
205201
) : null }
206-
{ accounts && accounts.map(acc => <GetAccountLink key={acc.code} {...acc} handleSidebarItemClick={this.handleSidebarItemClick }/>) }
202+
203+
{
204+
accountsByKeystore.map(keystore => (<React.Fragment key={keystore.keystore.rootFingerprint}>
205+
<div className={style.sidebarHeaderContainer}>
206+
<span className={style.sidebarHeader} hidden={!keystores?.length}>
207+
{t('sidebar.accounts')} - {keystore.keystore.name}
208+
</span>
209+
</div>
210+
211+
{ keystore.accounts.map(acc => <GetAccountLink key={acc.code} {...acc} handleSidebarItemClick={this.handleSidebarItemClick }/>) }
212+
</React.Fragment>))
213+
}
214+
207215
<div className={[style.sidebarHeaderContainer, style.end].join(' ')}></div>
208216
{ accounts.length ? (
209217
<div key="buy" className={style.sidebarItem}>

frontends/web/src/routes/account/utils.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { CoinCode, ScriptType, IAccount, CoinUnit } from '../../api/account';
18+
import { CoinCode, ScriptType, IAccount, CoinUnit, TKeystore } from '../../api/account';
1919

2020
export function findAccount(accounts: IAccount[], accountCode: string): IAccount | undefined {
2121
return accounts.find(({ code }) => accountCode === code);
@@ -94,3 +94,23 @@ export function customFeeUnit(coinCode: CoinCode): string {
9494
}
9595
return '';
9696
}
97+
98+
export type TAccountsByKeystore = {
99+
keystore: TKeystore;
100+
accounts: IAccount[];
101+
};
102+
103+
// Returns the accounts grouped by the keystore fingerprint.
104+
export function getAccountsByKeystore(accounts: IAccount[]): TAccountsByKeystore[] {
105+
return Object.values(accounts.reduce((acc, account) => {
106+
const key = account.keystore.rootFingerprint;
107+
if (!acc[key]) {
108+
acc[key] = {
109+
keystore: account.keystore,
110+
accounts: []
111+
};
112+
}
113+
acc[key].accounts.push(account);
114+
return acc;
115+
}, {} as Record<string, TAccountsByKeystore>));
116+
}

frontends/web/src/routes/settings/manage-accounts.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import React, { Component } from 'react';
18+
import { getAccountsByKeystore } from '../account/utils';
1819
import { route } from '../../utils/route';
1920
import * as accountAPI from '../../api/account';
2021
import * as backendAPI from '../../api/backend';
@@ -68,8 +69,8 @@ class ManageAccounts extends Component<Props, State> {
6869
this.fetchAccounts();
6970
}
7071

71-
private renderAccounts = () => {
72-
const { accounts, showTokens } = this.state;
72+
private renderAccounts = (accounts: accountAPI.IAccount[]) => {
73+
const { showTokens } = this.state;
7374
const { t } = this.props;
7475
return accounts.filter(account => !account.isToken).map(account => {
7576
const active = account.active;
@@ -216,8 +217,8 @@ class ManageAccounts extends Component<Props, State> {
216217

217218
public render() {
218219
const { t, deviceIDs, hasAccounts } = this.props;
219-
const { editAccountCode, editAccountNewName, editErrorMessage } = this.state;
220-
const accountList = this.renderAccounts();
220+
const { accounts, editAccountCode, editAccountNewName, editErrorMessage } = this.state;
221+
const accountsByKeystore = getAccountsByKeystore(accounts);
221222
return (
222223
<GuideWrapper>
223224
<GuidedContent>
@@ -240,9 +241,17 @@ class ManageAccounts extends Component<Props, State> {
240241
onClick={() => route('/add-account', true)}>
241242
{t('addAccount.title')}
242243
</Button>
243-
<div className="box slim divide m-bottom-large">
244-
{ (accountList && accountList.length) ? accountList : t('manageAccounts.noAccounts') }
245-
</div>
244+
245+
{
246+
accountsByKeystore.map(keystore => (<React.Fragment key={keystore.keystore.rootFingerprint}>
247+
<p>{keystore.keystore.name}</p>
248+
<div className="box slim divide m-bottom-large">
249+
{ this.renderAccounts(keystore.accounts) }
250+
</div>
251+
</React.Fragment>))
252+
}
253+
{ accounts.length === 0 ? t('manageAccounts.noAccounts') : null }
254+
246255
<Dialog
247256
open={!!(editAccountCode)}
248257
onClose={() => this.setState({ editAccountCode: undefined, editAccountNewName: '', editErrorMessage: undefined })}

0 commit comments

Comments
 (0)