Skip to content

Commit 122f59d

Browse files
committed
feat(): bump
1 parent 868d22d commit 122f59d

File tree

2 files changed

+126
-30
lines changed

2 files changed

+126
-30
lines changed

src/app/pages/account-add/account-add.page.ts

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -101,38 +101,41 @@ export class AccountAddPage {
101101
this.formValid = true
102102
}
103103

104-
selectedProtocols.forEach((wrapper) => {
105-
wrapper.customDerivationPath = undefined
106-
})
104+
// Calculate next derivation path for each selected protocol
105+
await Promise.all(
106+
selectedProtocols.map(async (wrapper) => {
107+
const nextPath = await this.secretsService.getNextDerivationPathForProtocol(wrapper.protocol, this.secret)
108+
wrapper.customDerivationPath = nextPath.derivationPath
109+
wrapper.isHDWallet = nextPath.isHDWallet
110+
})
111+
)
107112

108113
if (selectedProtocols.length === 1) {
109114
this.singleSelectedProtocol = selectedProtocols[0]
110-
this.singleSelectedProtocol.customDerivationPath = await this.singleSelectedProtocol.protocol.getStandardDerivationPath()
111115
} else {
112-
if (this.singleSelectedProtocol) {
113-
this.singleSelectedProtocol.customDerivationPath = undefined
114-
}
115116
this.singleSelectedProtocol = undefined
116117
}
117118
}, 0)
118119
}
119120

120121
public async toggleHDWallet() {
121122
// "isHDWallet" can only be toggled if one protocol is checked
122-
123+
// When toggled, recalculate the derivation path for the new HD/non-HD mode
123124
const selectedProtocols = this.protocolList.filter((protocol) => protocol.isChecked)
124125
if (selectedProtocols.length === 1) {
125126
const selectedProtocol = selectedProtocols[0]
126-
const standardDerivationPath = await selectedProtocol.protocol.getStandardDerivationPath()
127-
if ((await selectedProtocol.protocol.getSupportsHD()) && selectedProtocol.isHDWallet) {
128-
selectedProtocol.customDerivationPath = standardDerivationPath
127+
// Recalculate the derivation path with the new isHDWallet value
128+
const nextPath = await this.secretsService.getNextDerivationPathForProtocol(selectedProtocol.protocol, this.secret)
129+
// The toggle has already changed isHDWallet, so we use that value
130+
if (selectedProtocol.isHDWallet) {
131+
// HD mode - use standard path with account increment
132+
const standardPath = await selectedProtocol.protocol.getStandardDerivationPath()
133+
selectedProtocol.customDerivationPath = nextPath.derivationPath.includes('/0/') ? standardPath : nextPath.derivationPath
129134
} else {
130-
selectedProtocol.customDerivationPath = `${standardDerivationPath}/0/0`
135+
// Non-HD mode - use full path with address index
136+
selectedProtocol.customDerivationPath = nextPath.derivationPath
131137
}
132-
}
133-
134-
if (selectedProtocols.length === 1) {
135-
this.singleSelectedProtocol = selectedProtocols[0]
138+
this.singleSelectedProtocol = selectedProtocol
136139
} else {
137140
this.singleSelectedProtocol = undefined
138141
}
@@ -160,29 +163,36 @@ export class AccountAddPage {
160163

161164
private async addWalletAndReturnToAddressPage(): Promise<void> {
162165
const addAccount = async () => {
166+
const selectedProtocols = this.protocolList.filter((p) => p.isChecked)
167+
163168
this.secretsService
164169
.addWallets(
165170
this.secret,
166171
await Promise.all(
167-
this.protocolList.map(async (protocolWrapper: ProtocolWrapper) => {
172+
selectedProtocols.map(async (protocolWrapper: ProtocolWrapper) => {
168173
const protocol = protocolWrapper.protocol
169174
return {
170175
protocolIdentifier: await protocol.getIdentifier(),
171-
isHDWallet: protocolWrapper.isChecked ? protocolWrapper.isHDWallet : await protocol.getSupportsHD(),
172-
customDerivationPath:
173-
protocolWrapper.isChecked && protocolWrapper.customDerivationPath
174-
? protocolWrapper.customDerivationPath
175-
: await protocol.getStandardDerivationPath(),
176-
bip39Passphrase: protocolWrapper.isChecked ? this.bip39Passphrase : '',
177-
isActive: protocolWrapper.isChecked
176+
isHDWallet: protocolWrapper.isHDWallet,
177+
customDerivationPath: protocolWrapper.customDerivationPath ?? (await protocol.getStandardDerivationPath()),
178+
bip39Passphrase: this.bip39Passphrase,
179+
isActive: true
178180
}
179181
})
180182
)
181183
)
182-
.then(() => {
183-
this.navigationService
184-
.routeWithState('/accounts-list', { secret: this.secret }, { replaceUrl: true })
185-
.catch(handleErrorLocal(ErrorCategory.IONIC_NAVIGATION))
184+
.then((createdWallets) => {
185+
if (createdWallets.length === 1) {
186+
// Navigate directly to the new account's detail page
187+
this.navigationService
188+
.routeWithState('/account-address', { wallet: createdWallets[0], secret: this.secret }, { replaceUrl: true })
189+
.catch(handleErrorLocal(ErrorCategory.IONIC_NAVIGATION))
190+
} else {
191+
// Multiple wallets or no wallets created, go to accounts list
192+
this.navigationService
193+
.routeWithState('/accounts-list', { secret: this.secret }, { replaceUrl: true })
194+
.catch(handleErrorLocal(ErrorCategory.IONIC_NAVIGATION))
195+
}
186196
})
187197
.catch(handleErrorLocal(ErrorCategory.SECURE_STORAGE))
188198
}

src/app/services/secrets/secrets.service.ts

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,83 @@ export class SecretsService {
260260
return walletList
261261
}
262262

263+
public async getWalletsByProtocolIdentifier(protocolIdentifier: ProtocolSymbols, secret?: MnemonicSecret): Promise<AirGapWallet[]> {
264+
const wallets = secret ? secret.wallets : this.getWallets()
265+
const filtered = await Promise.all(
266+
wallets.map(async (wallet) => {
267+
const identifier = await wallet.protocol.getIdentifier()
268+
return identifier === protocolIdentifier && wallet.status === AirGapWalletStatus.ACTIVE ? wallet : undefined
269+
})
270+
)
271+
return filtered.filter((w): w is AirGapWallet => w !== undefined)
272+
}
273+
274+
private isBtcProtocol(protocolIdentifier: ProtocolSymbols): boolean {
275+
return (
276+
protocolIdentifier === MainProtocolSymbols.BTC ||
277+
protocolIdentifier === MainProtocolSymbols.BTC_SEGWIT ||
278+
protocolIdentifier === MainProtocolSymbols.BTC_TAPROOT
279+
)
280+
}
281+
282+
public async getNextDerivationPathForProtocol(
283+
protocol: ICoinProtocol,
284+
secret: MnemonicSecret
285+
): Promise<{ derivationPath: string; isHDWallet: boolean }> {
286+
const protocolIdentifier = await protocol.getIdentifier()
287+
const existingWallets = await this.getWalletsByProtocolIdentifier(protocolIdentifier, secret)
288+
const standardPath = await protocol.getStandardDerivationPath()
289+
const isBtc = this.isBtcProtocol(protocolIdentifier)
290+
const supportsHD = await protocol.getSupportsHD()
291+
292+
if (existingWallets.length === 0) {
293+
// First wallet - use standard path
294+
// For BTC and HD-capable protocols, use HD wallet
295+
// For non-HD protocols, use non-HD wallet
296+
return { derivationPath: standardPath, isHDWallet: isBtc || supportsHD }
297+
}
298+
299+
if (isBtc) {
300+
// BTC protocols: Always HD, increment account index
301+
// e.g., m/44'/0'/0' -> m/44'/0'/1'
302+
const lastIndices = existingWallets.map((wallet) => {
303+
const match = wallet.derivationPath.match(/(\d+)[h']?\/?$/)
304+
return match ? parseInt(match[1], 10) : 0
305+
})
306+
const maxIndex = Math.max(...lastIndices)
307+
const nextIndex = maxIndex + 1
308+
const newPath = standardPath.replace(/(\d+)([h']?)(\/?)?$/, `${nextIndex}$2$3`)
309+
return { derivationPath: newPath, isHDWallet: true }
310+
} else if (supportsHD) {
311+
// HD-capable protocols (ETH, OP, etc.): First is HD, subsequent are non-HD
312+
// First wallet at m/44'/60'/0' is equivalent to m/44'/60'/0'/0/0
313+
// Subsequent wallets use m/44'/60'/0'/0/1, m/44'/60'/0'/0/2, etc.
314+
const addressIndices = existingWallets.map((wallet) => {
315+
if (wallet.isExtendedPublicKey) {
316+
// HD wallet is equivalent to /0/0
317+
return 0
318+
}
319+
// Non-HD wallet - extract last number from path
320+
const match = wallet.derivationPath.match(/\/(\d+)$/)
321+
return match ? parseInt(match[1], 10) : 0
322+
})
323+
const maxIndex = Math.max(...addressIndices)
324+
const nextIndex = maxIndex + 1
325+
return { derivationPath: `${standardPath}/0/${nextIndex}`, isHDWallet: false }
326+
} else {
327+
// Non-HD protocols: Increment last number in path
328+
// e.g., m/44h/1729h/0h/0h -> m/44h/1729h/0h/1h
329+
const lastIndices = existingWallets.map((wallet) => {
330+
const match = wallet.derivationPath.match(/(\d+)[h']?\/?$/)
331+
return match ? parseInt(match[1], 10) : 0
332+
})
333+
const maxIndex = Math.max(...lastIndices)
334+
const nextIndex = maxIndex + 1
335+
const newPath = standardPath.replace(/(\d+)([h']?)(\/?)?$/, `${nextIndex}$2$3`)
336+
return { derivationPath: newPath, isHDWallet: false }
337+
}
338+
}
339+
263340
public async removeWallets(wallets: AirGapWallet[]): Promise<void[]> {
264341
return Promise.all(wallets.map((wallet) => this.removeWallet(wallet)))
265342
}
@@ -456,7 +533,7 @@ export class SecretsService {
456533
await this.addOrUpdateSecret(secret)
457534
}
458535

459-
public async addWallets(secret: MnemonicSecret, configs: AddWalletConifg[]): Promise<void> {
536+
public async addWallets(secret: MnemonicSecret, configs: AddWalletConifg[]): Promise<AirGapWallet[]> {
460537
const loading: HTMLIonLoadingElement = await this.loadingCtrl.create({
461538
message: 'Deriving your wallet...'
462539
})
@@ -465,18 +542,27 @@ export class SecretsService {
465542
try {
466543
const entropy: string = await this.retrieveEntropyForSecret(secret)
467544

545+
const activeConfigs = configs.filter((config) => config.isActive)
546+
468547
const createdOrUpdated: Either<AirGapWallet, AirGapWallet>[] = (
469-
await Promise.all(configs.map((config: AddWalletConifg) => this.activateOrCreateWallet(entropy, config)))
548+
await Promise.all(activeConfigs.map((config: AddWalletConifg) => this.activateOrCreateWallet(entropy, config)))
470549
).filter((createdOrUpdated: Either<AirGapWallet, AirGapWallet> | undefined) => createdOrUpdated !== undefined)
471550

472551
const [createdWallets, updatedWallets]: [AirGapWallet[], AirGapWallet[]] = merged(createdOrUpdated)
473552

474553
if (createdWallets.length > 0 || updatedWallets.length > 0) {
475554
secret.wallets.push(...createdWallets)
476555
await this.addOrUpdateSecret(secret)
556+
} else if (activeConfigs.length > 0) {
557+
this.showAlert(
558+
'Account already exists',
559+
'This account already exists with the same derivation path. The account was not added.'
560+
).catch(handleErrorLocal(ErrorCategory.IONIC_ALERT))
477561
}
478562

479563
loading.dismiss().catch(handleErrorLocal(ErrorCategory.IONIC_LOADER))
564+
565+
return [...createdWallets, ...updatedWallets]
480566
} catch (error) {
481567
loading.dismiss().catch(handleErrorLocal(ErrorCategory.IONIC_LOADER))
482568

0 commit comments

Comments
 (0)