Skip to content

Commit 0788606

Browse files
AngelCastilloBgreatertomi
authored andcommitted
feat(web-extension): added support to coin purpose in accounts
1 parent e6861d7 commit 0788606

File tree

6 files changed

+151
-24
lines changed

6 files changed

+151
-24
lines changed

packages/web-extension/src/walletManager/SigningCoordinator/SigningCoordinator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export class SigningCoordinator<WalletMetadata extends {}, AccountMetadata exten
165165
],
166166
extendedAccountPublicKey: account.extendedAccountPublicKey,
167167
getPassphrase: async () => passphrase,
168-
purpose: KeyPurpose.STANDARD
168+
purpose: account.purpose || KeyPurpose.STANDARD
169169
})
170170
);
171171
clearPassphrase(passphrase);

packages/web-extension/src/walletManager/WalletRepository/WalletRepository.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
WalletRepositoryApi
88
} from './types';
99
import { AnyWallet, ScriptWallet, WalletId, WalletType } from '../types';
10+
import { KeyPurpose } from '@cardano-sdk/key-management';
1011
import { Logger } from 'ts-log';
1112
import { Observable, defer, firstValueFrom, map, shareReplay, switchMap, take } from 'rxjs';
1213
import { WalletConflictError } from '../errors';
@@ -28,16 +29,24 @@ const cloneSplice = <T>(array: T[], start: number, deleteCount: number, ...items
2829
const findAccount = <WalletMetadata extends {}, AccountMetadata extends {}>(
2930
wallets: AnyWallet<WalletMetadata, AccountMetadata>[],
3031
walletId: WalletId,
31-
accountIndex: number
32+
accountIndex: number,
33+
purpose: KeyPurpose
3234
) => {
3335
const walletIdx = wallets.findIndex((w) => w.walletId === walletId);
3436
const wallet = wallets[walletIdx];
37+
3538
if (!wallet || wallet.type === WalletType.Script) return;
36-
const accountIdx = wallet.accounts.findIndex((acc) => acc.accountIndex === accountIndex);
39+
40+
const accountIdx = wallet.accounts.findIndex((acc) => {
41+
const accountPurpose = acc.purpose || KeyPurpose.STANDARD;
42+
return acc.accountIndex === accountIndex && accountPurpose === purpose;
43+
});
44+
3745
if (accountIdx < 0) return;
3846
return {
3947
account: wallet.accounts[accountIdx],
4048
accountIdx,
49+
purpose,
4150
wallet,
4251
walletIdx
4352
};
@@ -96,6 +105,8 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
96105
addAccount(props: AddAccountProps<AccountMetadata>): Promise<AddAccountProps<AccountMetadata>> {
97106
const { walletId, accountIndex, metadata, extendedAccountPublicKey } = props;
98107
this.#logger.debug('addAccount', walletId, accountIndex, metadata);
108+
const purpose = props.purpose || KeyPurpose.STANDARD;
109+
99110
return firstValueFrom(
100111
this.#getWallets().pipe(
101112
switchMap((wallets) => {
@@ -107,8 +118,16 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
107118
if (wallet.type === WalletType.Script) {
108119
throw new WalletConflictError('addAccount for script wallets is not supported');
109120
}
110-
if (wallet.accounts.some((acc) => acc.accountIndex === accountIndex)) {
111-
throw new WalletConflictError(`Account #${accountIndex} for wallet '${walletId}' already exists`);
121+
122+
if (
123+
wallet.accounts.some((acc) => {
124+
const accountPurpose = acc.purpose || KeyPurpose.STANDARD;
125+
return acc.accountIndex === accountIndex && accountPurpose === purpose;
126+
})
127+
) {
128+
throw new WalletConflictError(
129+
`Account #${accountIndex} with purpose ${purpose} for wallet '${walletId}' already exists`
130+
);
112131
}
113132

114133
return this.#store
@@ -120,7 +139,8 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
120139
{
121140
accountIndex,
122141
extendedAccountPublicKey,
123-
metadata
142+
metadata,
143+
purpose: props.purpose
124144
}
125145
]
126146
})
@@ -136,6 +156,7 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
136156
): Promise<UpdateWalletMetadataProps<WalletMetadata>> {
137157
const { walletId, metadata } = props;
138158
this.#logger.debug('updateWalletMetadata', walletId, metadata);
159+
139160
return firstValueFrom(
140161
this.#getWallets().pipe(
141162
switchMap((wallets) => {
@@ -160,14 +181,17 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
160181
props: UpdateAccountMetadataProps<AccountMetadata>
161182
): Promise<UpdateAccountMetadataProps<AccountMetadata>> {
162183
const { walletId, accountIndex, metadata } = props;
163-
this.#logger.debug('updateAccountMetadata', walletId, accountIndex, metadata);
184+
const purpose = props.purpose || KeyPurpose.STANDARD;
185+
186+
this.#logger.debug('updateAccountMetadata', walletId, accountIndex, metadata, purpose);
187+
164188
return firstValueFrom(
165189
this.#getWallets().pipe(
166190
switchMap((wallets) => {
167191
// update account
168-
const bip32Account = findAccount(wallets, walletId, accountIndex);
192+
const bip32Account = findAccount(wallets, walletId, accountIndex, purpose);
169193
if (!bip32Account) {
170-
throw new WalletConflictError(`Account not found: ${walletId}/${accountIndex}`);
194+
throw new WalletConflictError(`Account not found: ${walletId}/${purpose}/${accountIndex}`);
171195
}
172196
return this.#store.setAll(
173197
cloneSplice(wallets, bip32Account.walletIdx, 1, {
@@ -185,23 +209,29 @@ export class WalletRepository<WalletMetadata extends {}, AccountMetadata extends
185209
}
186210

187211
removeAccount(props: RemoveAccountProps): Promise<RemoveAccountProps> {
188-
const { walletId, accountIndex } = props;
189-
this.#logger.debug('removeAccount', walletId, accountIndex);
212+
const { walletId, accountIndex, purpose: maybePurpose } = props;
213+
214+
const purpose = maybePurpose || KeyPurpose.STANDARD;
215+
216+
this.#logger.debug('removeAccount', walletId, accountIndex, purpose);
190217
return firstValueFrom(
191218
this.#getWallets().pipe(
192219
switchMap((wallets) => {
193-
const bip32Account = findAccount(wallets, walletId, accountIndex);
220+
const bip32Account = findAccount(wallets, walletId, accountIndex, purpose);
194221
if (!bip32Account) {
195-
throw new WalletConflictError(`Account '${walletId}/${accountIndex}' does not exist`);
222+
throw new WalletConflictError(`Account '${walletId}/${purpose}/${accountIndex}' does not exist`);
196223
}
197224
const dependentWallet = wallets.find(
198225
(wallet) =>
199226
wallet.type === WalletType.Script &&
200-
wallet.ownSigners.some((signer) => signer.walletId === walletId && signer.accountIndex === accountIndex)
227+
wallet.ownSigners.some(
228+
(signer) =>
229+
signer.walletId === walletId && signer.accountIndex === accountIndex && signer.purpose === purpose
230+
)
201231
);
202232
if (dependentWallet) {
203233
throw new WalletConflictError(
204-
`Wallet '${dependentWallet.walletId}' depends on account '${walletId}/${accountIndex}'`
234+
`Wallet '${dependentWallet.walletId}' depends on account '${walletId}/${purpose}/${accountIndex}'`
205235
);
206236
}
207237
return this.#store.setAll(

packages/web-extension/src/walletManager/WalletRepository/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { AnyWallet, HardwareWallet, InMemoryWallet, ScriptWallet, WalletId } from '../types';
22
import { Bip32PublicKeyHex } from '@cardano-sdk/crypto';
3+
import { KeyPurpose } from '@cardano-sdk/key-management';
34
import { Observable } from 'rxjs';
45

56
export type RemoveAccountProps = {
67
walletId: WalletId;
78
/** account' in cip1852 */
89
accountIndex: number;
10+
purpose?: KeyPurpose;
911
};
1012

1113
export type AddAccountProps<Metadata extends {}> = {
1214
walletId: WalletId;
1315
/** account' in cip1852 */
1416
accountIndex: number;
17+
purpose?: KeyPurpose;
1518
metadata: Metadata;
1619
extendedAccountPublicKey: Bip32PublicKeyHex;
1720
};
@@ -24,6 +27,7 @@ export type UpdateWalletMetadataProps<Metadata extends {}> = {
2427
export type UpdateAccountMetadataProps<Metadata extends {}> = {
2528
/** account' in cip1852; must be specified for bip32 wallets */
2629
walletId: WalletId;
30+
purpose?: KeyPurpose;
2731
accountIndex: number;
2832
metadata: Metadata;
2933
};

packages/web-extension/src/walletManager/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AccountKeyDerivationPath } from '@cardano-sdk/key-management';
1+
import { AccountKeyDerivationPath, KeyPurpose } from '@cardano-sdk/key-management';
22
import { Bip32PublicKeyHex } from '@cardano-sdk/crypto';
33
import { Cardano } from '@cardano-sdk/core';
44
import { HexBlob } from '@cardano-sdk/util';
@@ -15,6 +15,7 @@ export type WalletId = string;
1515

1616
export type Bip32WalletAccount<Metadata extends {}> = {
1717
accountIndex: number;
18+
purpose?: KeyPurpose;
1819
/** e.g. account name, picture */
1920
metadata: Metadata;
2021
extendedAccountPublicKey: Bip32PublicKeyHex;
@@ -55,6 +56,7 @@ export type AnyBip32Wallet<WalletMetadata extends {}, AccountMetadata extends {}
5556

5657
export type OwnSignerAccount = {
5758
walletId: WalletId;
59+
purpose: KeyPurpose;
5860
accountIndex: number;
5961
stakingScriptKeyPath: AccountKeyDerivationPath;
6062
paymentScriptKeyPath: AccountKeyDerivationPath;

packages/web-extension/test/walletManager/WalletRepository.test.ts

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import {
1111
WalletRepositoryDependencies,
1212
WalletType
1313
} from '../../src';
14+
import { BehaviorSubject, firstValueFrom, of } from 'rxjs';
1415
import { Cardano, Serialization } from '@cardano-sdk/core';
1516
import { Hash28ByteBase16 } from '@cardano-sdk/crypto';
16-
import { KeyRole } from '@cardano-sdk/key-management';
17-
import { firstValueFrom, of } from 'rxjs';
17+
import { KeyPurpose, KeyRole } from '@cardano-sdk/key-management';
1818
import { logger } from '@cardano-sdk/util-dev';
1919
import pick from 'lodash/pick';
2020

@@ -41,6 +41,7 @@ const createScriptWalletProps = {
4141
index: 0,
4242
role: KeyRole.External
4343
},
44+
purpose: KeyPurpose.STANDARD,
4445
stakingScriptKeyPath: {
4546
index: 0,
4647
role: KeyRole.External
@@ -136,6 +137,7 @@ describe('WalletRepository', () => {
136137
index: 0,
137138
role: KeyRole.External
138139
},
140+
purpose: KeyPurpose.STANDARD,
139141
stakingScriptKeyPath: {
140142
index: 0,
141143
role: KeyRole.External
@@ -158,6 +160,7 @@ describe('WalletRepository', () => {
158160
index: 0,
159161
role: KeyRole.External
160162
},
163+
purpose: KeyPurpose.STANDARD,
161164
stakingScriptKeyPath: {
162165
index: 0,
163166
role: KeyRole.External
@@ -188,11 +191,9 @@ describe('WalletRepository', () => {
188191
});
189192

190193
describe('addAccount', () => {
194+
const accountIndex = storedLedgerWallet.accounts[storedLedgerWallet.accounts.length - 1].accountIndex + 1;
191195
it('adds account to an existing wallet and returns AccountId that also contains walletId', async () => {
192-
const accountProps = createAccount(
193-
0,
194-
storedLedgerWallet.accounts[storedLedgerWallet.accounts.length - 1].accountIndex + 1
195-
);
196+
const accountProps = createAccount(0, accountIndex);
196197
const props = {
197198
...accountProps,
198199
walletId: storedLedgerWallet.walletId
@@ -206,6 +207,42 @@ describe('WalletRepository', () => {
206207
]);
207208
});
208209

210+
it('allows creating 1852 and 1854 purpose account with the same index', async () => {
211+
const storeSubject = new BehaviorSubject([storedLedgerWallet]);
212+
store.observeAll.mockReturnValue(storeSubject.asObservable());
213+
214+
const standardAccountProps = createAccount(0, accountIndex, KeyPurpose.STANDARD);
215+
const standardProps = {
216+
...standardAccountProps,
217+
walletId: storedLedgerWallet.walletId
218+
};
219+
const walletWithStandardAccount = [
220+
{
221+
...storedLedgerWallet,
222+
accounts: [...storedLedgerWallet.accounts, standardAccountProps]
223+
}
224+
];
225+
226+
const multiSigAccountProps = createAccount(0, accountIndex, KeyPurpose.MULTI_SIG);
227+
const multiSigProps = {
228+
...multiSigAccountProps,
229+
walletId: storedLedgerWallet.walletId
230+
};
231+
232+
await expect(repository.addAccount(standardProps)).resolves.toEqual(standardProps);
233+
expect(store.setAll).toBeCalledWith(walletWithStandardAccount);
234+
235+
storeSubject.next(walletWithStandardAccount);
236+
237+
await expect(repository.addAccount(multiSigProps)).resolves.toEqual(multiSigProps);
238+
expect(store.setAll).toBeCalledWith([
239+
{
240+
...storedLedgerWallet,
241+
accounts: [...walletWithStandardAccount[0].accounts, multiSigAccountProps]
242+
}
243+
]);
244+
});
245+
209246
it('rejects with WalletConflictError when wallet is not found', async () => {
210247
await expect(
211248
repository.addAccount({
@@ -290,6 +327,34 @@ describe('WalletRepository', () => {
290327
]);
291328
});
292329

330+
it('does not update 1852 account metadata when updating 1854 account', async () => {
331+
const storedAccount = storedLedgerWallet.accounts[0];
332+
const newAccount = createAccount(0, storedAccount.accountIndex, KeyPurpose.MULTI_SIG);
333+
const accounts = [storedAccount, newAccount];
334+
store.observeAll.mockReturnValueOnce(of([{ ...storedLedgerWallet, accounts }]));
335+
336+
const props: UpdateAccountMetadataProps<WalletMetadata> = {
337+
accountIndex: newAccount.accountIndex,
338+
metadata: newMetadata,
339+
purpose: KeyPurpose.MULTI_SIG,
340+
walletId: storedLedgerWallet.walletId
341+
};
342+
343+
await expect(repository.updateAccountMetadata(props)).resolves.toEqual(props);
344+
expect(store.setAll).toBeCalledWith([
345+
{
346+
...storedLedgerWallet,
347+
accounts: [
348+
storedAccount,
349+
{
350+
...newAccount,
351+
metadata: newMetadata
352+
}
353+
]
354+
}
355+
]);
356+
});
357+
293358
it('rejects with WalletConflictError when a bip32 account or a script wallet with specified id is not found', async () => {
294359
await expect(
295360
repository.updateWalletMetadata({
@@ -344,6 +409,26 @@ describe('WalletRepository', () => {
344409
]);
345410
});
346411

412+
it('does not remove 1852 account when removing 1854 account', async () => {
413+
const storedAccount = storedLedgerWallet.accounts[0];
414+
const newAccount = createAccount(0, storedAccount.accountIndex, KeyPurpose.MULTI_SIG);
415+
const accounts = [storedAccount, newAccount];
416+
store.observeAll.mockReturnValueOnce(of([{ ...storedLedgerWallet, accounts }]));
417+
418+
const props = {
419+
accountIndex: newAccount.accountIndex,
420+
purpose: KeyPurpose.MULTI_SIG,
421+
walletId: storedLedgerWallet.walletId
422+
};
423+
await expect(repository.removeAccount(props)).resolves.toEqual(props);
424+
expect(store.setAll).toBeCalledWith([
425+
{
426+
...storedLedgerWallet,
427+
accounts: [storedAccount]
428+
}
429+
]);
430+
});
431+
347432
it('rejects with WalletConflictError when account is not found', async () => {
348433
await expect(
349434
repository.removeAccount({ accountIndex: 0, walletId: 'doesnt exist' as Hash28ByteBase16 })
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Bip32PublicKeyHex } from '@cardano-sdk/crypto';
22
import { Bip32WalletAccount } from '../../src';
3+
import { KeyPurpose } from '@cardano-sdk/key-management';
34

45
export type WalletMetadata = { name: string };
56
export type AccountMetadata = { name: string };
@@ -9,8 +10,13 @@ export const createPubKey = (numWallet: number, accountIndex: number) =>
910
`${numWallet}a4f80dea2632a17c99ae9d8b934abf02643db5426b889fef14709c85e294aa12ac1f1560a893ea7937c5bfbfdeab459b1a396f1174b9c5a673a640d01880c3${accountIndex}`
1011
);
1112

12-
export const createAccount = (numWallet: number, accountIndex: number): Bip32WalletAccount<AccountMetadata> => ({
13+
export const createAccount = (
14+
numWallet: number,
15+
accountIndex: number,
16+
purpose: KeyPurpose = KeyPurpose.STANDARD
17+
): Bip32WalletAccount<AccountMetadata> => ({
1318
accountIndex,
1419
extendedAccountPublicKey: createPubKey(numWallet, accountIndex),
15-
metadata: { name: `Wallet ${numWallet} Account #${accountIndex}` }
20+
metadata: { name: `Wallet ${numWallet} Account #${accountIndex}` },
21+
purpose
1622
});

0 commit comments

Comments
 (0)