Skip to content

Commit ccc9f84

Browse files
committed
Merge branch 'feat/bump-derivation' into 'develop'
feat(): bump See merge request papers/airgap/airgap-vault!549
2 parents 868d22d + 1fd311f commit ccc9f84

File tree

2 files changed

+124
-30
lines changed

2 files changed

+124
-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: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,81 @@ 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+
const lastIndices = existingWallets.map((wallet) => {
302+
const match = wallet.derivationPath.match(/(\d+)[h']?\/?$/)
303+
return match ? parseInt(match[1], 10) : 0
304+
})
305+
const maxIndex = Math.max(...lastIndices)
306+
const nextIndex = maxIndex + 1
307+
const newPath = standardPath.replace(/(\d+)([h']?)(\/?)?$/, `${nextIndex}$2$3`)
308+
return { derivationPath: newPath, isHDWallet: true }
309+
} else if (supportsHD) {
310+
// HD-capable protocols (ETH, OP, etc.): First is HD, subsequent are non-HD
311+
312+
const addressIndices = existingWallets.map((wallet) => {
313+
if (wallet.isExtendedPublicKey) {
314+
// HD wallet is equivalent to /0/0
315+
return 0
316+
}
317+
// Non-HD wallet - extract last number from path
318+
const match = wallet.derivationPath.match(/\/(\d+)$/)
319+
return match ? parseInt(match[1], 10) : 0
320+
})
321+
const maxIndex = Math.max(...addressIndices)
322+
const nextIndex = maxIndex + 1
323+
return { derivationPath: `${standardPath}/0/${nextIndex}`, isHDWallet: false }
324+
} else {
325+
// Non-HD protocols: Increment last number in path
326+
// e.g., m/44h/1729h/0h/0h -> m/44h/1729h/0h/1h
327+
const lastIndices = existingWallets.map((wallet) => {
328+
const match = wallet.derivationPath.match(/(\d+)[h']?\/?$/)
329+
return match ? parseInt(match[1], 10) : 0
330+
})
331+
const maxIndex = Math.max(...lastIndices)
332+
const nextIndex = maxIndex + 1
333+
const newPath = standardPath.replace(/(\d+)([h']?)(\/?)?$/, `${nextIndex}$2$3`)
334+
return { derivationPath: newPath, isHDWallet: false }
335+
}
336+
}
337+
263338
public async removeWallets(wallets: AirGapWallet[]): Promise<void[]> {
264339
return Promise.all(wallets.map((wallet) => this.removeWallet(wallet)))
265340
}
@@ -456,7 +531,7 @@ export class SecretsService {
456531
await this.addOrUpdateSecret(secret)
457532
}
458533

459-
public async addWallets(secret: MnemonicSecret, configs: AddWalletConifg[]): Promise<void> {
534+
public async addWallets(secret: MnemonicSecret, configs: AddWalletConifg[]): Promise<AirGapWallet[]> {
460535
const loading: HTMLIonLoadingElement = await this.loadingCtrl.create({
461536
message: 'Deriving your wallet...'
462537
})
@@ -465,18 +540,27 @@ export class SecretsService {
465540
try {
466541
const entropy: string = await this.retrieveEntropyForSecret(secret)
467542

543+
const activeConfigs = configs.filter((config) => config.isActive)
544+
468545
const createdOrUpdated: Either<AirGapWallet, AirGapWallet>[] = (
469-
await Promise.all(configs.map((config: AddWalletConifg) => this.activateOrCreateWallet(entropy, config)))
546+
await Promise.all(activeConfigs.map((config: AddWalletConifg) => this.activateOrCreateWallet(entropy, config)))
470547
).filter((createdOrUpdated: Either<AirGapWallet, AirGapWallet> | undefined) => createdOrUpdated !== undefined)
471548

472549
const [createdWallets, updatedWallets]: [AirGapWallet[], AirGapWallet[]] = merged(createdOrUpdated)
473550

474551
if (createdWallets.length > 0 || updatedWallets.length > 0) {
475552
secret.wallets.push(...createdWallets)
476553
await this.addOrUpdateSecret(secret)
554+
} else if (activeConfigs.length > 0) {
555+
this.showAlert(
556+
'Account already exists',
557+
'This account already exists with the same derivation path. The account was not added.'
558+
).catch(handleErrorLocal(ErrorCategory.IONIC_ALERT))
477559
}
478560

479561
loading.dismiss().catch(handleErrorLocal(ErrorCategory.IONIC_LOADER))
562+
563+
return [...createdWallets, ...updatedWallets]
480564
} catch (error) {
481565
loading.dismiss().catch(handleErrorLocal(ErrorCategory.IONIC_LOADER))
482566

0 commit comments

Comments
 (0)