Skip to content

Commit bce6ac4

Browse files
committed
Merge branch 'develop' into 'master'
Develop See merge request papers/airgap/airgap-vault!550
2 parents fe15e03 + 2dfb9da commit bce6ac4

File tree

7 files changed

+606
-320
lines changed

7 files changed

+606
-320
lines changed

package.json

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -37,51 +37,55 @@
3737
"apply-diagnostic-modules": "node apply-diagnostic-modules.js"
3838
},
3939
"resolutions": {
40-
"@airgap/aeternity": "0.13.44",
41-
"@airgap/acurast": "0.13.44",
42-
"@airgap/astar": "0.13.44",
43-
"@airgap/bitcoin": "0.13.44",
44-
"@airgap/coinlib-core": "0.13.44",
45-
"@airgap/coreum": "0.13.44",
46-
"@airgap/cosmos": "0.13.44",
47-
"@airgap/cosmos-core": "0.13.44",
48-
"@airgap/crypto": "0.13.44",
49-
"@airgap/ethereum": "0.13.44",
50-
"@airgap/groestlcoin": "0.13.44",
51-
"@airgap/icp": "0.13.44",
52-
"@airgap/module-kit": "0.13.44",
53-
"@airgap/moonbeam": "0.13.44",
54-
"@airgap/optimism": "0.13.44",
55-
"@airgap/polkadot": "0.13.44",
56-
"@airgap/serializer": "0.13.44",
57-
"@airgap/stellar": "0.13.44",
58-
"@airgap/substrate": "0.13.44",
59-
"@airgap/tezos": "0.13.44"
40+
"@airgap/aeternity": "0.13.45",
41+
"@airgap/acurast": "0.13.45",
42+
"@airgap/astar": "0.13.45",
43+
"@airgap/bitcoin": "0.13.45",
44+
"@airgap/coinlib-core": "0.13.45",
45+
"@airgap/coreum": "0.13.45",
46+
"@airgap/cosmos": "0.13.45",
47+
"@airgap/cosmos-core": "0.13.45",
48+
"@airgap/crypto": "0.13.45",
49+
"@airgap/ethereum": "0.13.45",
50+
"@airgap/groestlcoin": "0.13.45",
51+
"@airgap/icp": "0.13.45",
52+
"@airgap/module-kit": "0.13.45",
53+
"@airgap/moonbeam": "0.13.45",
54+
"@airgap/optimism": "0.13.45",
55+
"@airgap/base": "0.13.45",
56+
"@airgap/bnb": "0.13.45",
57+
"@airgap/polkadot": "0.13.45",
58+
"@airgap/serializer": "0.13.45",
59+
"@airgap/stellar": "0.13.45",
60+
"@airgap/substrate": "0.13.45",
61+
"@airgap/tezos": "0.13.45"
6062
},
6163
"dependencies": {
62-
"@airgap/aeternity": "0.13.44",
63-
"@airgap/acurast": "0.13.44",
64-
"@airgap/angular-core": "0.0.60",
65-
"@airgap/angular-ngrx": "0.0.60",
66-
"@airgap/astar": "0.13.44",
67-
"@airgap/bitcoin": "0.13.44",
68-
"@airgap/coinlib-core": "0.13.44",
69-
"@airgap/coreum": "0.13.44",
70-
"@airgap/cosmos": "0.13.44",
71-
"@airgap/cosmos-core": "0.13.44",
72-
"@airgap/crypto": "0.13.44",
73-
"@airgap/ethereum": "0.13.44",
74-
"@airgap/groestlcoin": "0.13.44",
75-
"@airgap/icp": "0.13.44",
76-
"@airgap/module-kit": "0.13.44",
77-
"@airgap/moonbeam": "0.13.44",
78-
"@airgap/optimism": "0.13.44",
79-
"@airgap/polkadot": "0.13.44",
64+
"@airgap/aeternity": "0.13.45",
65+
"@airgap/acurast": "0.13.45",
66+
"@airgap/angular-core": "0.0.61",
67+
"@airgap/angular-ngrx": "0.0.61",
68+
"@airgap/astar": "0.13.45",
69+
"@airgap/bitcoin": "0.13.45",
70+
"@airgap/coinlib-core": "0.13.45",
71+
"@airgap/coreum": "0.13.45",
72+
"@airgap/cosmos": "0.13.45",
73+
"@airgap/cosmos-core": "0.13.45",
74+
"@airgap/crypto": "0.13.45",
75+
"@airgap/ethereum": "0.13.45",
76+
"@airgap/groestlcoin": "0.13.45",
77+
"@airgap/icp": "0.13.45",
78+
"@airgap/module-kit": "0.13.45",
79+
"@airgap/moonbeam": "0.13.45",
80+
"@airgap/optimism": "0.13.45",
81+
"@airgap/base": "0.13.45",
82+
"@airgap/bnb": "0.13.45",
83+
"@airgap/polkadot": "0.13.45",
8084
"@airgap/sapling-wasm": "0.0.7",
81-
"@airgap/serializer": "0.13.44",
82-
"@airgap/stellar": "0.13.44",
83-
"@airgap/substrate": "0.13.44",
84-
"@airgap/tezos": "0.13.44",
85+
"@airgap/serializer": "0.13.45",
86+
"@airgap/stellar": "0.13.45",
87+
"@airgap/substrate": "0.13.45",
88+
"@airgap/tezos": "0.13.45",
8589
"@airgap-community/iso-rootstock": "1.0.0",
8690
"@angular/cdk": "^14.2.7",
8791
"@angular/common": "16.1.1",

src/app/app.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { GroestlcoinModule } from '@airgap/groestlcoin'
1818
import { ICPModule } from '@airgap/icp'
1919
import { MoonbeamModule } from '@airgap/moonbeam'
2020
import { OptimismModule } from '@airgap/optimism'
21+
import { BnbModule } from '@airgap/bnb'
22+
import { BaseModule } from '@airgap/base'
2123
import { PolkadotModule } from '@airgap/polkadot'
2224
import { TezosModule, TezosSaplingExternalMethodProvider, TezosShieldedTezProtocol } from '@airgap/tezos'
2325
import { AcurastModule } from '@airgap/acurast'
@@ -190,6 +192,8 @@ export class AppComponent implements AfterViewInit {
190192
new ICPModule(),
191193
new CoreumModule(),
192194
new OptimismModule(),
195+
new BnbModule(),
196+
new BaseModule(),
193197
new AcurastModule(),
194198
new StellarModule()
195199
])

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/pages/account-address/account-address.page.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ export class AccountAddressPage {
154154
break
155155
case MainProtocolSymbols.ETH:
156156
case MainProtocolSymbols.OPTIMISM:
157+
case MainProtocolSymbols.BNB:
158+
case MainProtocolSymbols.BASE:
157159
this.syncOptions = [airgapwallet]
158160
if (this.wallet.isExtendedPublicKey) {
159161
this.syncOptions.push(metamask, imtoken, rabby)

src/app/services/iac/iac.service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ export class IACService extends BaseIACService {
217217
// ETH: MetaMask requests don't contain the public key information, we need to do the matching based on the sourceFingerprint
218218
if (
219219
!correctWallet &&
220-
(signTransactionRequest.protocol === MainProtocolSymbols.ETH || signTransactionRequest.protocol === MainProtocolSymbols.OPTIMISM)
220+
(signTransactionRequest.protocol === MainProtocolSymbols.ETH ||
221+
signTransactionRequest.protocol === MainProtocolSymbols.OPTIMISM ||
222+
signTransactionRequest.protocol === MainProtocolSymbols.BNB ||
223+
signTransactionRequest.protocol === MainProtocolSymbols.BASE)
221224
) {
222225
const transaction: RawTypedEthereumTransaction = unsignedTransaction.transaction
223226

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)