From 850bc767cbaff01b40802ec3b8f152aa3d781cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Tue, 4 Feb 2025 14:43:52 +0100 Subject: [PATCH] refactor: change how addresses are derived --- .nvmrc | 2 +- package.json | 18 +- src/background/models.ts | 7 +- src/background/runtime/BackgroundRuntime.ts | 4 + .../services/accounts/AccountsService.test.ts | 296 ++++--- .../services/accounts/AccountsService.ts | 83 +- .../avalanche_getAddressesInRange.test.ts | 13 +- .../handlers/avalanche_getAddressesInRange.ts | 18 +- .../accounts/handlers/getPrivateKey.ts | 4 +- src/background/services/accounts/models.ts | 6 + .../services/accounts/utils/mapVMAddresses.ts | 12 + .../services/accounts/utils/typeGuards.ts | 2 +- .../getTotalBalanceForWallet.test.ts | 10 +- .../getTotalBalanceForWallet.ts | 16 +- .../fireblocksUpdateApiCredentials.test.ts | 1 + ...migrateMissingPublicKeysFromLedger.test.ts | 128 ++- .../migrateMissingPublicKeysFromLedger.ts | 98 ++- .../keystoneOnboardingHandler.test.ts | 10 +- .../handlers/keystoneOnboardingHandler.ts | 10 +- .../handlers/ledgerOnboardingHandler.test.ts | 67 +- .../handlers/ledgerOnboardingHandler.ts | 54 +- .../mnemonicOnboardingHandler.test.ts | 19 +- .../handlers/mnemonicOnboardingHandler.ts | 16 +- .../seedlessOnboardingHandler.test.ts | 26 +- .../handlers/seedlessOnboardingHandler.ts | 12 +- .../services/secrets/AddressPublicKey.ts | 170 ++++ .../services/secrets/AddressResolver.ts | 157 ++++ .../services/secrets/SecretsService.test.ts | 780 ++++++------------ .../services/secrets/SecretsService.ts | 502 +++++------ src/background/services/secrets/models.ts | 71 +- src/background/services/secrets/utils.ts | 116 +++ .../services/seedless/SeedlessWallet.test.ts | 116 +-- .../services/seedless/SeedlessWallet.ts | 62 +- .../cancelRecoveryPhraseExport.test.ts | 7 +- .../handlers/cancelRecoveryPhraseExport.ts | 2 +- .../completeRecoveryPhraseExport.test.ts | 7 +- .../handlers/completeRecoveryPhraseExport.ts | 2 +- .../getRecoveryPhraseExportState.test.ts | 7 +- .../handlers/getRecoveryPhraseExportState.ts | 2 +- .../handlers/initRecoveryPhraseExport.test.ts | 7 +- .../handlers/initRecoveryPhraseExport.ts | 2 +- .../services/storage/StorageService.test.ts | 7 + .../services/storage/StorageService.ts | 10 +- .../wallet_v4/utils/getSecretsType.ts | 1 + .../migrations/wallet_v5/commonModels.ts | 38 + .../migrations/wallet_v5/commonSchemas.ts | 48 ++ .../migrations/wallet_v5/legacyModels.ts | 75 ++ .../migrations/wallet_v5/legacySchema.ts | 83 ++ .../migrations/wallet_v5/newModels.ts | 91 ++ .../migrations/wallet_v5/newSchema.ts | 93 +++ .../migrations/wallet_v5/wallet_v5.ts | 310 +++++++ .../storage/schemaMigrations/models.ts | 43 + .../storage/schemaMigrations/schemaMap.ts | 24 +- .../schemaMigrations/schemaMigrations.ts | 24 +- .../services/wallet/WalletService.test.ts | 225 +++-- .../services/wallet/WalletService.ts | 286 +++---- .../wallet/handlers/importLedger.test.ts | 73 +- .../services/wallet/handlers/importLedger.ts | 95 ++- .../wallet/handlers/importSeedPhrase.test.ts | 39 +- .../wallet/handlers/importSeedPhrase.ts | 16 +- .../storeBtcWalletPolicyDetails.test.ts | 6 +- .../handlers/storeBtcWalletPolicyDetails.ts | 6 +- .../vmModules/ApprovalController.test.ts | 28 +- .../vmModules/ApprovalController.ts | 23 +- src/background/vmModules/ModuleManager.ts | 1 - src/hooks/useErrorMessage.ts | 39 +- src/localization/locales/en/translation.json | 8 + .../ApproveAction/TxBatchApprovalScreen.tsx | 2 +- src/tests/test-utils.tsx | 17 + src/utils/assertions.ts | 15 +- src/utils/errors/errorCodes.ts | 27 +- src/utils/object.ts | 13 + yarn.lock | 158 ++-- 73 files changed, 3338 insertions(+), 1528 deletions(-) create mode 100644 src/background/services/accounts/utils/mapVMAddresses.ts create mode 100644 src/background/services/secrets/AddressPublicKey.ts create mode 100644 src/background/services/secrets/AddressResolver.ts create mode 100644 src/background/services/secrets/utils.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonModels.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonSchemas.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacyModels.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacySchema.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/wallet_v5/newModels.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/wallet_v5/newSchema.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/wallet_v5/wallet_v5.ts create mode 100644 src/utils/object.ts diff --git a/.nvmrc b/.nvmrc index 7ea6a59d3..67e145bf0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.11.0 +v20.18.0 diff --git a/package.json b/package.json index 6f20ba8fc..ad8504231 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "sentry": "node sentryscript.js" }, "dependencies": { - "@avalabs/avalanche-module": "1.2.1", + "@avalabs/avalanche-module": "0.0.0-feat-solana-address-resolution-20250205161605", "@avalabs/avalanchejs": "4.1.2-alpha.3", - "@avalabs/bitcoin-module": "1.2.1", + "@avalabs/bitcoin-module": "0.0.0-feat-solana-address-resolution-20250205161605", "@avalabs/bridge-unified": "4.0.1", "@avalabs/core-bridge-sdk": "3.1.0-alpha.32", "@avalabs/core-chains-sdk": "3.1.0-alpha.32", @@ -37,12 +37,12 @@ "@avalabs/core-token-prices-sdk": "3.1.0-alpha.32", "@avalabs/core-utils-sdk": "3.1.0-alpha.32", "@avalabs/core-wallets-sdk": "3.1.0-alpha.32", - "@avalabs/evm-module": "1.2.1", + "@avalabs/evm-module": "0.0.0-feat-solana-address-resolution-20250205161605", "@avalabs/glacier-sdk": "3.1.0-alpha.32", "@avalabs/hw-app-avalanche": "0.14.1", - "@avalabs/hvm-module": "1.2.1", + "@avalabs/hvm-module": "0.0.0-feat-solana-address-resolution-20250205161605", "@avalabs/types": "3.1.0-alpha.32", - "@avalabs/vm-module-types": "1.2.1", + "@avalabs/vm-module-types": "0.0.0-feat-solana-address-resolution-20250205161605", "@blockaid/client": "0.10.0", "@coinbase/cbpay-js": "1.6.0", "@cubist-labs/cubesigner-sdk": "0.3.28", @@ -61,6 +61,7 @@ "@noble/curves": "1.6.0", "@noble/hashes": "1.3.2", "@openzeppelin/contracts": "4.9.6", + "@scure/base": "1.2.4", "@sentry/browser": "7.66.0", "@sentry/integrations": "7.66.0", "@sentry/react": "7.66.0", @@ -87,6 +88,7 @@ "ledger-bitcoin": "0.2.3", "lodash": "4.17.21", "lru-cache": "7.10.1", + "micro-key-producer": "0.7.5", "micro-signals": "2.4.0", "paraswap": "5.1.0", "qrcode.react": "1.0.1", @@ -223,7 +225,7 @@ "*.{ts,tsx}": "eslint --fix --max-warnings 0" }, "volta": { - "node": "20.11.0", + "node": "20.18.0", "yarn": "1.22.19" }, "lavamoat": { @@ -270,7 +272,9 @@ "@avalabs/core-bridge-sdk>@avalabs/core-wallets-sdk>@metamask/eth-sig-util>@metamask/utils>@ethereumjs/tx>@ethereumjs/common>ethereumjs-util>ethereum-cryptography>keccak": false, "@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>bufferutil": false, "@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": false, - "@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>eciesjs>secp256k1": false + "@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>eciesjs>secp256k1": false, + "@avalabs/avalanche-module>@avalabs/core-wallets-sdk>@metamask/eth-sig-util>ethereumjs-abi>ethereumjs-util>ethereum-cryptography>keccak": false, + "@avalabs/avalanche-module>@avalabs/core-wallets-sdk>@metamask/eth-sig-util>ethereumjs-abi>ethereumjs-util>ethereum-cryptography>secp256k1": false } } } diff --git a/src/background/models.ts b/src/background/models.ts index ff18808b8..849154350 100644 --- a/src/background/models.ts +++ b/src/background/models.ts @@ -87,9 +87,6 @@ export type FirstParameter any> = T extends ( export const ACTION_HANDLED_BY_MODULE = '__handled.via.vm.modules__'; -export const hasDefined = ( - obj: T, - key: K, -): obj is EnsureDefined => { - return obj[key] !== undefined; +export type ExcludeUndefined> = { + [K in keyof T as T[K] extends undefined ? never : K]: T[K]; }; diff --git a/src/background/runtime/BackgroundRuntime.ts b/src/background/runtime/BackgroundRuntime.ts index 872ac2b25..5ff341d82 100644 --- a/src/background/runtime/BackgroundRuntime.ts +++ b/src/background/runtime/BackgroundRuntime.ts @@ -6,6 +6,7 @@ import { LockService } from '@src/background/services/lock/LockService'; import { OnboardingService } from '@src/background/services/onboarding/OnboardingService'; import { ModuleManager } from '../vmModules/ModuleManager'; import { BridgeService } from '../services/bridge/BridgeService'; +import { AddressResolver } from '../services/secrets/AddressResolver'; @singleton() export class BackgroundRuntime { @@ -16,6 +17,7 @@ export class BackgroundRuntime { // we try to fetch the bridge configs as soon as possible private bridgeService: BridgeService, private moduleManager: ModuleManager, + private addressResolver: AddressResolver, ) {} activate() { @@ -28,6 +30,8 @@ export class BackgroundRuntime { this.lockService.activate(); this.onboardingService.activate(); this.moduleManager.activate(); + + this.addressResolver.init(this.moduleManager); } private onInstalled() { diff --git a/src/background/services/accounts/AccountsService.test.ts b/src/background/services/accounts/AccountsService.test.ts index 5e2974d8f..cb27a7832 100644 --- a/src/background/services/accounts/AccountsService.test.ts +++ b/src/background/services/accounts/AccountsService.test.ts @@ -9,20 +9,28 @@ import { ImportType, ImportData, } from './models'; -import { NetworkVMType } from '@avalabs/core-chains-sdk'; import { WalletConnectStorage } from '../walletConnect/WalletConnectStorage'; import { WalletConnectService } from '../walletConnect/WalletConnectService'; import { PermissionsService } from '../permissions/PermissionsService'; import { SecretsService } from '../secrets/SecretsService'; +import { emptyAddresses } from '../secrets/utils'; import { isProductionBuild } from '@src/utils/environment'; import { AnalyticsServicePosthog } from '../analytics/AnalyticsServicePosthog'; import { SecretType } from '../secrets/models'; +import { AddressResolver } from '../secrets/AddressResolver'; +import { ModuleManager } from '@src/background/vmModules/ModuleManager'; +import { DerivationPath } from '@avalabs/core-wallets-sdk'; +import { mapVMAddresses } from './utils/mapVMAddresses'; +import { expectToThrowErroCode } from '@src/tests/test-utils'; +import { AccountError } from '@src/utils/errors'; +import { NetworkVMType } from '@avalabs/vm-module-types'; jest.mock('../storage/StorageService'); jest.mock('../secrets/SecretsService'); jest.mock('../ledger/LedgerService'); jest.mock('../lock/LockService'); jest.mock('../permissions/PermissionsService'); +jest.mock('../secrets/AddressResolver'); jest.mock('../analytics/utils/encryptAnalyticsData'); jest.mock('@src/utils/environment'); @@ -39,6 +47,10 @@ describe('background/services/accounts/AccountsService', () => { new WalletConnectStorage(storageService), ); const secretsService = new SecretsService(storageService); + const addressResolver = new AddressResolver(networkService, secretsService); + addressResolver.init({ + loadModuleByNetwork: jest.fn(), + } as unknown as ModuleManager); const permissionsService = new PermissionsService({} as any); @@ -65,7 +77,7 @@ describe('background/services/accounts/AccountsService', () => { const coreEthAddress = 'C-'; const otherEvmAddress = '0x000000001'; const otherBtcAddress = 'btc000000001'; - const hvmAddress = undefined; + const hvmAddress = 'hvm0000001'; const getAllAddresses = (useOtherAddresses = false) => ({ addressC: useOtherAddresses ? otherEvmAddress : evmAddress, @@ -76,6 +88,15 @@ describe('background/services/accounts/AccountsService', () => { addressHVM: hvmAddress, }); + const getAllAddressesByVMs = (useOtherAddresses = false) => ({ + [NetworkVMType.EVM]: useOtherAddresses ? otherEvmAddress : evmAddress, + [NetworkVMType.BITCOIN]: useOtherAddresses ? otherBtcAddress : btcAddress, + [NetworkVMType.AVM]: avmAddress, + [NetworkVMType.PVM]: pvmAddress, + [NetworkVMType.CoreEth]: coreEthAddress, + [NetworkVMType.HVM]: hvmAddress, + }); + const mockAccounts = ( withAddresses = false, withOtherAddresses = false, @@ -130,6 +151,11 @@ describe('background/services/accounts/AccountsService', () => { id: 'fb-acc', name: 'Fireblocks account', type: AccountType.FIREBLOCKS, + ...(withAddresses + ? ({ + addressC: withOtherAddresses ? otherEvmAddress : evmAddress, + } as any) + : {}), }, }; @@ -143,18 +169,32 @@ describe('background/services/accounts/AccountsService', () => { }; }; + const mockAddressResolution = (useOtherAddresses = false) => { + const mockedSecrets = { + derivationPathSpec: DerivationPath.BIP44, + } as any; + jest + .mocked(secretsService.getPrimaryAccountSecrets) + .mockResolvedValue(mockedSecrets); + jest + .mocked(addressResolver.getAddressesForSecretId) + .mockImplementation(async (id) => + id === 'fb-acc' + ? ({ + [NetworkVMType.EVM]: useOtherAddresses + ? otherEvmAddress + : evmAddress, + } as any) + : getAllAddressesByVMs(useOtherAddresses), + ); + }; + beforeEach(() => { jest.resetAllMocks(); (storageService.load as jest.Mock).mockResolvedValue(emptyAccounts); analyticsServicePosthog.captureEncryptedEvent = jest.fn(); - (secretsService.addAddress as jest.Mock).mockResolvedValue({ - [NetworkVMType.EVM]: evmAddress, - [NetworkVMType.BITCOIN]: btcAddress, - [NetworkVMType.AVM]: avmAddress, - [NetworkVMType.PVM]: pvmAddress, - [NetworkVMType.CoreEth]: coreEthAddress, - [NetworkVMType.HVM]: hvmAddress, - }); + (secretsService.addAddress as jest.Mock).mockResolvedValue(undefined); + addressResolver.getAddressesForSecretId = jest.fn(); networkService.developerModeChanged.add = jest.fn(); networkService.developerModeChanged.remove = jest.fn(); accountsService = new AccountsService( @@ -165,6 +205,7 @@ describe('background/services/accounts/AccountsService', () => { secretsService, ledgerService, walletConnectService, + addressResolver, ); }); @@ -174,6 +215,8 @@ describe('background/services/accounts/AccountsService', () => { (storageService.load as jest.Mock).mockResolvedValue(mockedAccounts); + mockAddressResolution(); + await accountsService.onUnlock(); const accounts = accountsService.getAccountList(); @@ -215,7 +258,12 @@ describe('background/services/accounts/AccountsService', () => { type: AccountType.IMPORTED, ...getAllAddresses(), }, - { id: 'fb-acc', name: 'Fireblocks account', type: 'fireblocks' }, + { + id: 'fb-acc', + name: 'Fireblocks account', + type: 'fireblocks', + addressC: evmAddress, + }, ]); }); }); @@ -232,104 +280,88 @@ describe('background/services/accounts/AccountsService', () => { it('init returns with accounts not from updating', async () => { const mockedAccounts = mockAccounts(true); (storageService.load as jest.Mock).mockResolvedValue(mockedAccounts); + mockAddressResolution(); await accountsService.onUnlock(); - expect(storageService.load).toBeCalledTimes(1); - expect(storageService.load).toBeCalledWith(ACCOUNTS_STORAGE_KEY); + expect(storageService.load).toHaveBeenCalledTimes(1); + expect(storageService.load).toHaveBeenCalledWith(ACCOUNTS_STORAGE_KEY); const accounts = accountsService.getAccounts(); expect(accounts).toStrictEqual(mockedAccounts); }); - it('init returns with accounts with missing addresses', async () => { + it('updates addresses when missing', async () => { const mockedAccounts = mockAccounts(true); (storageService.load as jest.Mock).mockResolvedValue(mockAccounts(false)); - (secretsService.getAddresses as jest.Mock).mockResolvedValue({ - [NetworkVMType.EVM]: evmAddress, - [NetworkVMType.BITCOIN]: btcAddress, - [NetworkVMType.AVM]: avmAddress, - [NetworkVMType.PVM]: pvmAddress, - [NetworkVMType.CoreEth]: coreEthAddress, - [NetworkVMType.HVM]: hvmAddress, - }); - (secretsService.getImportedAddresses as jest.Mock) - .mockResolvedValueOnce({ ...mockedAccounts.imported['0x1'], id: '0x1' }) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['0x2'], - id: '0x2', - }); + mockAddressResolution(); await accountsService.onUnlock(); - expect(storageService.load).toBeCalledTimes(1); - expect(storageService.load).toBeCalledWith(ACCOUNTS_STORAGE_KEY); - expect(secretsService.getAddresses).toBeCalledTimes(3); - expect(secretsService.getAddresses).toHaveBeenNthCalledWith( + expect(storageService.load).toHaveBeenCalledTimes(1); + expect(storageService.load).toHaveBeenCalledWith(ACCOUNTS_STORAGE_KEY); + expect(addressResolver.getAddressesForSecretId).toHaveBeenCalledTimes(6); + expect(addressResolver.getAddressesForSecretId).toHaveBeenNthCalledWith( 1, - 0, - walletId, - networkService, + '0x1', ); - expect(secretsService.getAddresses).toHaveBeenNthCalledWith( + expect(addressResolver.getAddressesForSecretId).toHaveBeenNthCalledWith( 2, - 1, + '0x2', + ); + expect(addressResolver.getAddressesForSecretId).toHaveBeenNthCalledWith( + 3, + 'fb-acc', + ); + expect(addressResolver.getAddressesForSecretId).toHaveBeenNthCalledWith( + 4, + walletId, + 0, + DerivationPath.BIP44, + ); + expect(addressResolver.getAddressesForSecretId).toHaveBeenNthCalledWith( + 5, walletId, - networkService, + 1, + DerivationPath.BIP44, + ); + expect(addressResolver.getAddressesForSecretId).toHaveBeenNthCalledWith( + 6, + secondaryWalletId, + 0, + DerivationPath.BIP44, ); - expect(secretsService.getImportedAddresses).toBeCalledTimes(3); const accounts = accountsService.getAccounts(); expect(accounts).toStrictEqual(mockedAccounts); }); - it('account addresses are updated on developer mode change', async () => { + it('updates addresses on developer mode change', async () => { const mockedAccounts = mockAccounts(true); - (storageService.load as jest.Mock).mockResolvedValue(mockAccounts(true)); - (secretsService.getAddresses as jest.Mock).mockResolvedValue({ - [NetworkVMType.EVM]: otherEvmAddress, - [NetworkVMType.BITCOIN]: otherBtcAddress, - [NetworkVMType.AVM]: avmAddress, - [NetworkVMType.PVM]: pvmAddress, - [NetworkVMType.CoreEth]: coreEthAddress, - [NetworkVMType.HVM]: hvmAddress, - }); - (secretsService.getImportedAddresses as jest.Mock) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['fb-acc'], - }) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['0x1'], - id: '0x1', - addressC: otherEvmAddress, - addressBTC: otherBtcAddress, - }) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['0x2'], - id: '0x2', - addressC: otherEvmAddress, - addressBTC: otherBtcAddress, - }) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['fb-acc'], - }); + + jest.mocked(storageService.load).mockResolvedValue(mockedAccounts); + + mockAddressResolution(); await accountsService.onUnlock(); - expect(secretsService.getAddresses).not.toBeCalled(); const accounts = accountsService.getAccounts(); expect(accounts).toStrictEqual(mockedAccounts); - expect(networkService.developerModeChanged.add).toBeCalledTimes(1); + mockAddressResolution(true); + + expect(networkService.developerModeChanged.add).toHaveBeenCalledTimes(1); + // this mocks a network change (networkService.developerModeChanged.add as jest.Mock).mock.calls[0][0](); await new Promise(process.nextTick); - const updatedAccounts = accountsService.getAccounts(); + expect(addressResolver.getAddressesForSecretId).toHaveBeenCalledTimes(7); + const updatedAccounts = accountsService.getAccounts(); expect(updatedAccounts).toStrictEqual(mockAccounts(true, true)); }); }); @@ -340,27 +372,31 @@ describe('background/services/accounts/AccountsService', () => { beforeEach(async () => { mockedAccounts = mockAccounts(true); jest.mocked(storageService.load).mockResolvedValue(mockedAccounts); - jest.mocked(secretsService.getAddresses).mockResolvedValue({ - [NetworkVMType.EVM]: otherEvmAddress, - [NetworkVMType.BITCOIN]: otherBtcAddress, - [NetworkVMType.AVM]: avmAddress, - [NetworkVMType.PVM]: pvmAddress, - [NetworkVMType.CoreEth]: coreEthAddress, - [NetworkVMType.HVM]: otherEvmAddress, - }); + + mockAddressResolution(); await accountsService.onUnlock(); }); it('correctly updates addresses for selected primary account', async () => { jest - .mocked(secretsService.getImportedAddresses) - .mockImplementation((id) => mockedAccounts.imported[id]); + .mocked(addressResolver.getAddressesForSecretId) + .mockReset() + .mockResolvedValue({ + [NetworkVMType.EVM]: otherEvmAddress, + [NetworkVMType.BITCOIN]: otherBtcAddress, + [NetworkVMType.AVM]: avmAddress, + [NetworkVMType.PVM]: pvmAddress, + [NetworkVMType.CoreEth]: coreEthAddress, + [NetworkVMType.HVM]: otherEvmAddress, + [NetworkVMType.SVM]: '', + }); + await accountsService.refreshAddressesForAccount( mockedAccounts.primary[walletId][0]?.id as string, ); - expect(secretsService.getAddresses).toHaveBeenCalledTimes(1); + expect(addressResolver.getAddressesForSecretId).toHaveBeenCalledTimes(1); expect(accountsService.getAccounts().primary[0]).toEqual( mockAccounts(true, true).primary[0], ); @@ -368,27 +404,20 @@ describe('background/services/accounts/AccountsService', () => { it('correctly updates addresses for selected imported account', async () => { jest - .mocked(secretsService.getImportedAddresses) - .mockImplementation((id) => { - if (id === 'fb-acc') { - return { - ...mockedAccounts.imported['fb-acc'], - addressC: 'addressC-new', - }; - } - - return mockedAccounts.imported[id]; - }); + .mocked(addressResolver.getAddressesForSecretId) + .mockResolvedValueOnce({ + ...emptyAddresses(), + [NetworkVMType.EVM]: 'addressC-new', + } as any); await accountsService.refreshAddressesForAccount('fb-acc'); - expect(secretsService.getImportedAddresses).toHaveBeenCalledWith( + expect(addressResolver.getAddressesForSecretId).toHaveBeenCalledWith( 'fb-acc', - networkService, ); - expect(secretsService.getAddresses).toHaveBeenCalledTimes(0); expect(accountsService.getAccounts().imported['fb-acc']).toEqual({ ...mockAccounts(true, true).imported['fb-acc'], + ...mapVMAddresses(emptyAddresses()), addressC: 'addressC-new', }); }); @@ -396,6 +425,7 @@ describe('background/services/accounts/AccountsService', () => { describe('when testnet mode gets enabled and fireblocks account is active', () => { beforeEach(() => { + mockAddressResolution(); jest.mocked(isProductionBuild).mockReturnValue(true); }); @@ -403,30 +433,6 @@ describe('background/services/accounts/AccountsService', () => { const mockedAccounts = mockAccounts(true, false, 'fb-acc'); jest.spyOn(accountsService, 'activateAccount'); jest.mocked(storageService.load).mockResolvedValue(mockedAccounts); - jest.mocked(secretsService.getAddresses).mockResolvedValue({ - [NetworkVMType.EVM]: otherEvmAddress, - [NetworkVMType.BITCOIN]: otherBtcAddress, - [NetworkVMType.AVM]: avmAddress, - [NetworkVMType.PVM]: pvmAddress, - [NetworkVMType.CoreEth]: coreEthAddress, - [NetworkVMType.HVM]: otherEvmAddress, - }); - jest - .mocked(secretsService.getImportedAddresses) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['0x1'], - addressC: otherEvmAddress, - addressBTC: otherBtcAddress, - }) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['0x2'], - addressC: otherEvmAddress, - addressBTC: otherBtcAddress, - }) - .mockResolvedValueOnce({ - ...mockedAccounts.imported['fb-acc'], - addressC: 'addressC', - }); await accountsService.onUnlock(); @@ -446,6 +452,10 @@ describe('background/services/accounts/AccountsService', () => { }); describe('onLock', () => { + beforeEach(() => { + mockAddressResolution(); + }); + it('clears accounts and subscriptions on lock', async () => { const mockedAccounts = mockAccounts(true); (storageService.load as jest.Mock).mockResolvedValue(mockedAccounts); @@ -478,6 +488,10 @@ describe('background/services/accounts/AccountsService', () => { }); describe('getAccounts', () => { + beforeEach(() => { + mockAddressResolution(); + }); + it('returns accounts', async () => { const mockedAccounts = mockAccounts(true); (storageService.load as jest.Mock).mockResolvedValue(mockedAccounts); @@ -489,19 +503,26 @@ describe('background/services/accounts/AccountsService', () => { }); describe('addPrimaryAccount()', () => { + beforeEach(() => { + mockAddressResolution(); + }); + it('should thrown an error because of missing addresses', async () => { const uuid = 'uuid'; (crypto.randomUUID as jest.Mock).mockReturnValue(uuid); await accountsService.onUnlock(); - (secretsService.addAddress as jest.Mock).mockResolvedValueOnce({}); + jest + .mocked(addressResolver.getAddressesForSecretId) + .mockResolvedValueOnce({} as any); - await expect( + await expectToThrowErroCode( accountsService.addPrimaryAccount({ name: 'Account name', walletId, }), - ).rejects.toThrow(new Error('The account has no EVM or BTC address')); + AccountError.EVMAddressNotFound, + ); }); it('adds account with index 0 when no accounts', async () => { const uuid = 'uuid'; @@ -521,8 +542,8 @@ describe('background/services/accounts/AccountsService', () => { expect(secretsService.addAddress).toBeCalledWith({ index: 0, walletId: WALLET_ID, - networkService, ledgerService, + addressResolver, }); const accounts = accountsService.getAccounts(); @@ -573,8 +594,8 @@ describe('background/services/accounts/AccountsService', () => { expect(secretsService.addAddress).toBeCalledWith({ index: 2, walletId: WALLET_ID, - networkService, ledgerService, + addressResolver, }); expect(permissionsService.addWhitelistDomains).toBeCalledTimes(1); expect(permissionsService.addWhitelistDomains).toBeCalledWith( @@ -614,8 +635,8 @@ describe('background/services/accounts/AccountsService', () => { expect(secretsService.addAddress).toBeCalledWith({ index: 2, walletId: WALLET_ID, - networkService, ledgerService, + addressResolver, }); expect(permissionsService.addWhitelistDomains).toBeCalledTimes(1); expect(permissionsService.addWhitelistDomains).toBeCalledWith( @@ -674,6 +695,10 @@ describe('background/services/accounts/AccountsService', () => { }); describe('addImportedAccount()', () => { + beforeEach(() => { + mockAddressResolution(); + }); + const uuidMock = 'some unique id'; const commitMock = jest.fn(); @@ -703,7 +728,7 @@ describe('background/services/accounts/AccountsService', () => { expect(secretsService.addImportedWallet).toBeCalledTimes(1); expect(secretsService.addImportedWallet).toBeCalledWith( options, - networkService, + addressResolver, ); expect(commitMock).toHaveBeenCalled(); expect(permissionsService.addWhitelistDomains).toBeCalledTimes(1); @@ -761,7 +786,7 @@ describe('background/services/accounts/AccountsService', () => { expect(secretsService.addImportedWallet).toBeCalledTimes(1); expect(secretsService.addImportedWallet).toBeCalledWith( options, - networkService, + addressResolver, ); expect(commitMock).toHaveBeenCalled(); expect(permissionsService.addWhitelistDomains).toBeCalledTimes(1); @@ -862,7 +887,7 @@ describe('background/services/accounts/AccountsService', () => { expect(secretsService.addImportedWallet).toBeCalledTimes(1); expect(secretsService.addImportedWallet).toBeCalledWith( options, - networkService, + addressResolver, ); expect(commitMock).not.toHaveBeenCalled(); expect(permissionsService.addWhitelistDomains).not.toHaveBeenCalled(); @@ -887,6 +912,10 @@ describe('background/services/accounts/AccountsService', () => { }); describe('setAccountName', () => { + beforeEach(() => { + mockAddressResolution(); + }); + it('throws error if account not found', async () => { await accountsService.onUnlock(); expect(accountsService.getAccounts()).toStrictEqual(emptyAccounts); @@ -971,6 +1000,10 @@ describe('background/services/accounts/AccountsService', () => { }); describe('activateAccount', () => { + beforeEach(() => { + mockAddressResolution(); + }); + it('throws error if account not found', async () => { const mockedAccounts = mockAccounts(true); (storageService.load as jest.Mock).mockResolvedValue(mockedAccounts); @@ -1027,6 +1060,10 @@ describe('background/services/accounts/AccountsService', () => { }); describe('deleteAccounts', () => { + beforeEach(() => { + mockAddressResolution(); + }); + it('removes the imported accounts and their secrets', async () => { const mockedAccounts = mockAccounts(true); (storageService.load as jest.Mock).mockResolvedValue(mockedAccounts); @@ -1050,6 +1087,7 @@ describe('background/services/accounts/AccountsService', () => { id: 'fb-acc', name: 'Fireblocks account', type: 'fireblocks', + addressC: evmAddress, }, }, }; diff --git a/src/background/services/accounts/AccountsService.ts b/src/background/services/accounts/AccountsService.ts index b9483d5d4..6c6839340 100644 --- a/src/background/services/accounts/AccountsService.ts +++ b/src/background/services/accounts/AccountsService.ts @@ -13,6 +13,7 @@ import { IMPORT_TYPE_TO_ACCOUNT_TYPE_MAP, PrimaryAccount, WalletId, + AccountWithOptionalAddresses, } from './models'; import { OnLock, OnUnlock } from '@src/background/runtime/lifecycleCallbacks'; import { NetworkService } from '../network/NetworkService'; @@ -28,6 +29,10 @@ import { LedgerService } from '../ledger/LedgerService'; import { WalletConnectService } from '../walletConnect/WalletConnectService'; import { Network } from '../network/models'; import { isDevnet } from '@src/utils/isDevnet'; +import { AddressResolver } from '../secrets/AddressResolver'; +import { assertPresent, assertPropDefined } from '@src/utils/assertions'; +import { AccountError, SecretsError } from '@src/utils/errors'; +import { mapVMAddresses } from './utils/mapVMAddresses'; type AddAccountParams = { walletId: string; @@ -92,6 +97,7 @@ export class AccountsService implements OnLock, OnUnlock { private secretsService: SecretsService, private ledgerService: LedgerService, private walletConnectService: WalletConnectService, + private addressResolver: AddressResolver, ) {} async onUnlock(): Promise { @@ -223,32 +229,38 @@ export class AccountsService implements OnLock, OnUnlock { }; }; - async getAddressesForAccount(account: Account): Promise { - if (account.type !== AccountType.PRIMARY) { - return this.secretsService.getImportedAddresses( - account.id, - this.networkService, + async getAddressesForAccount( + account: AccountWithOptionalAddresses, + ): Promise { + if (isPrimaryAccount(account)) { + const secrets = + await this.secretsService.getPrimaryAccountSecrets(account); + + assertPresent(secrets, SecretsError.SecretsNotFound); + + const addresses = await this.addressResolver.getAddressesForSecretId( + account.walletId, + account.index, + secrets.derivationPathSpec, ); - } - const addresses = await this.secretsService.getAddresses( - account.index, - account.walletId, - this.networkService, - ); + assertPresent( + addresses[NetworkVMType.EVM], + AccountError.EVMAddressNotFound, + ); - if (!addresses[NetworkVMType.EVM]) { - throw new Error('The account has no EVM address'); + return mapVMAddresses(addresses); } + const addresses = await this.addressResolver.getAddressesForSecretId( + account.id, + ); - return { - addressC: addresses[NetworkVMType.EVM], - addressBTC: addresses[NetworkVMType.BITCOIN], - addressAVM: addresses[NetworkVMType.AVM], - addressPVM: addresses[NetworkVMType.PVM], - addressCoreEth: addresses[NetworkVMType.CoreEth], - addressHVM: addresses[NetworkVMType.HVM], - }; + assertPresent( + addresses[NetworkVMType.EVM], + AccountError.EVMAddressNotFound, + ); + + return mapVMAddresses(addresses); } async refreshAddressesForAccount(accountId: string): Promise { @@ -365,25 +377,26 @@ export class AccountsService implements OnLock, OnUnlock { const lastAccount = selectedWalletAccounts.at(-1); const nextIndex = lastAccount ? lastAccount.index + 1 : 0; + const id = crypto.randomUUID(); const newAccount = { + id, index: nextIndex, name: `Account ${nextIndex + 1}`, type: AccountType.PRIMARY as const, walletId: walletId, }; - const addresses = await this.secretsService.addAddress({ + await this.secretsService.addAddress({ index: nextIndex, walletId, - networkService: this.networkService, ledgerService: this.ledgerService, + addressResolver: this.addressResolver, }); - if (!addresses[NetworkVMType.EVM] || !addresses[NetworkVMType.BITCOIN]) { - throw new Error('The account has no EVM or BTC address'); - } + const addresses = await this.getAddressesForAccount(newAccount); - const id = crypto.randomUUID(); + assertPropDefined(addresses, 'addressC', AccountError.EVMAddressNotFound); + assertPropDefined(addresses, 'addressBTC', AccountError.BTCAddressNotFound); this.accounts = { ...this.accounts, @@ -393,20 +406,12 @@ export class AccountsService implements OnLock, OnUnlock { ...selectedWalletAccounts, { ...newAccount, - id, - addressC: addresses[NetworkVMType.EVM], - addressBTC: addresses[NetworkVMType.BITCOIN], - addressAVM: addresses[NetworkVMType.AVM], - addressPVM: addresses[NetworkVMType.PVM], - addressCoreEth: addresses[NetworkVMType.CoreEth], - addressHVM: addresses[NetworkVMType.HVM], + ...addresses, }, ], }, }; - await this.permissionsService.addWhitelistDomains( - addresses[NetworkVMType.EVM], - ); + await this.permissionsService.addWhitelistDomains(addresses.addressC); this.analyticsServicePosthog.captureEncryptedEvent({ name: 'addedNewPrimaryAccount', @@ -426,9 +431,11 @@ export class AccountsService implements OnLock, OnUnlock { try { const { account, commit } = await this.secretsService.addImportedWallet( options, - this.networkService, + this.addressResolver, ); + assertPropDefined(account, 'addressC', AccountError.EVMAddressNotFound); + const existingAccount = this.#findAccountByAddress(account.addressC); // If the account already exists for some reason, just return its ID. diff --git a/src/background/services/accounts/handlers/avalanche_getAddressesInRange.test.ts b/src/background/services/accounts/handlers/avalanche_getAddressesInRange.test.ts index c8dfdc9ad..13bf0ab05 100644 --- a/src/background/services/accounts/handlers/avalanche_getAddressesInRange.test.ts +++ b/src/background/services/accounts/handlers/avalanche_getAddressesInRange.test.ts @@ -7,6 +7,10 @@ import { canSkipApproval } from '@src/utils/canSkipApproval'; import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; import { AccountsService } from '../AccountsService'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; jest.mock('@avalabs/core-wallets-sdk'); jest.mock('@src/utils/canSkipApproval'); @@ -76,7 +80,14 @@ describe('background/services/accounts/handlers/avalanche_getAddressesInRange.ts beforeEach(() => { getPrimaryAccountSecretsMock.mockResolvedValue({ - xpubXP, + secretType: SecretType.Mnemonic, + extendedPublicKeys: [ + { + curve: 'secp256k1', + derivationPath: AVALANCHE_BASE_DERIVATION_PATH, + key: xpubXP, + }, + ], }); jest diff --git a/src/background/services/accounts/handlers/avalanche_getAddressesInRange.ts b/src/background/services/accounts/handlers/avalanche_getAddressesInRange.ts index f2b75545f..e4728932f 100644 --- a/src/background/services/accounts/handlers/avalanche_getAddressesInRange.ts +++ b/src/background/services/accounts/handlers/avalanche_getAddressesInRange.ts @@ -21,6 +21,8 @@ type Params = [ ]; import { AccountsService } from '../AccountsService'; import { getAddressesInRange } from '../utils/getAddressesInRange'; +import { getExtendedPublicKey } from '../../secrets/utils'; +import { AVALANCHE_BASE_DERIVATION_PATH } from '../../secrets/models'; const EXPOSED_DOMAINS = [ 'develop.avacloud-app.pages.dev', @@ -69,10 +71,20 @@ export class AvalancheGetAddressesInRangeHandler extends DAppRequestHandler< internal: [], }; - if (secrets?.xpubXP) { + if (!secrets || !('extendedPublicKeys' in secrets)) { + return addresses; + } + + const extendedPublicKey = getExtendedPublicKey( + secrets.extendedPublicKeys, + AVALANCHE_BASE_DERIVATION_PATH, + 'secp256k1', + ); + + if (extendedPublicKey) { if (externalLimit > 0) { addresses.external = getAddressesInRange( - secrets.xpubXP, + extendedPublicKey.key, provXP, false, externalStart, @@ -82,7 +94,7 @@ export class AvalancheGetAddressesInRangeHandler extends DAppRequestHandler< if (internalLimit > 0) { addresses.internal = getAddressesInRange( - secrets.xpubXP, + extendedPublicKey.key, provXP, true, internalStart, diff --git a/src/background/services/accounts/handlers/getPrivateKey.ts b/src/background/services/accounts/handlers/getPrivateKey.ts index 8f391cf8c..df4f45911 100644 --- a/src/background/services/accounts/handlers/getPrivateKey.ts +++ b/src/background/services/accounts/handlers/getPrivateKey.ts @@ -144,7 +144,7 @@ export class GetPrivateKeyHandler implements HandlerType { const pvmNode = master.derivePath( getAddressDerivationPath( accountIndex, - primaryAccount.derivationPath, + primaryAccount.derivationPathSpec, 'PVM', ), ); @@ -170,7 +170,7 @@ export class GetPrivateKeyHandler implements HandlerType { result: getAccountPrivateKeyFromMnemonic( primaryAccount.mnemonic, accountIndex, - primaryAccount.derivationPath, + primaryAccount.derivationPathSpec, ), }; } catch (e) { diff --git a/src/background/services/accounts/models.ts b/src/background/services/accounts/models.ts index c459e8795..2eca56cd7 100644 --- a/src/background/services/accounts/models.ts +++ b/src/background/services/accounts/models.ts @@ -1,3 +1,4 @@ +import { PartialBy } from '@avalabs/vm-module-types'; import { PubKeyType } from '../wallet/models'; export enum AccountType { @@ -147,3 +148,8 @@ export enum PrivateKeyChain { C = 'C', XP = 'XP', } + +export type AccountWithOptionalAddresses = PartialBy< + Account, + Extract +>; diff --git a/src/background/services/accounts/utils/mapVMAddresses.ts b/src/background/services/accounts/utils/mapVMAddresses.ts new file mode 100644 index 000000000..bbeede0bb --- /dev/null +++ b/src/background/services/accounts/utils/mapVMAddresses.ts @@ -0,0 +1,12 @@ +import { NetworkVMType } from '@avalabs/vm-module-types'; +import { omitUndefined } from '@src/utils/object'; + +export const mapVMAddresses = (addresses: Record) => + omitUndefined({ + addressC: addresses[NetworkVMType.EVM], + addressBTC: addresses[NetworkVMType.BITCOIN] || undefined, + addressAVM: addresses[NetworkVMType.AVM] || undefined, + addressPVM: addresses[NetworkVMType.PVM] || undefined, + addressCoreEth: addresses[NetworkVMType.CoreEth] || undefined, + addressHVM: addresses[NetworkVMType.HVM] || undefined, + } as const); diff --git a/src/background/services/accounts/utils/typeGuards.ts b/src/background/services/accounts/utils/typeGuards.ts index d0fa38479..43e9a04a5 100644 --- a/src/background/services/accounts/utils/typeGuards.ts +++ b/src/background/services/accounts/utils/typeGuards.ts @@ -17,7 +17,7 @@ export const isWalletConnectAccount = ( account?.type === AccountType.WALLET_CONNECT; export const isPrimaryAccount = ( - account?: Account, + account?: Pick, ): account is PrimaryAccount => account?.type === AccountType.PRIMARY; export const isImportedAccount = ( diff --git a/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.test.ts b/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.test.ts index 71ba1c849..eadacecec 100644 --- a/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.test.ts +++ b/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.test.ts @@ -21,6 +21,11 @@ import type { BalanceAggregatorService } from '../../BalanceAggregatorService'; import { getAccountsWithActivity } from './helpers'; import { IMPORTED_ACCOUNTS_WALLET_ID } from './models'; import { GetTotalBalanceForWalletHandler } from './getTotalBalanceForWallet'; +import { buildExtendedPublicKey } from '@src/background/services/secrets/utils'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + SecretType, +} from '@src/background/services/secrets/models'; jest.mock('./helpers/getAccountsWithActivity'); @@ -107,7 +112,10 @@ describe('background/services/balances/handlers/getTotalBalanceForWallet.test.ts const mockSecrets = (xpubXP?: string) => { secretsService.getWalletAccountsSecretsById.mockResolvedValueOnce({ - xpubXP, + secretType: SecretType.Mnemonic, + extendedPublicKeys: xpubXP + ? [buildExtendedPublicKey(xpubXP, AVALANCHE_BASE_DERIVATION_PATH)] + : [], } as any); }; diff --git a/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.ts b/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.ts index 4dd53f500..e474575b2 100644 --- a/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.ts +++ b/src/background/services/balances/handlers/getTotalBalanceForWallet/getTotalBalanceForWallet.ts @@ -24,6 +24,8 @@ import { getIncludedNetworks, } from './helpers'; import { getXPChainIds } from '@src/utils/getDefaultChainIds'; +import { getExtendedPublicKey } from '@src/background/services/secrets/utils'; +import { AVALANCHE_BASE_DERIVATION_PATH } from '@src/background/services/secrets/models'; type HandlerType = ExtensionRequestHandler< ExtensionRequest.BALANCES_GET_TOTAL_FOR_WALLET, @@ -59,10 +61,20 @@ export class GetTotalBalanceForWalletHandler implements HandlerType { const derivedAddressesUnprefixed = derivedWalletAddresses.map((addr) => addr.replace(/^[PXC]-/i, ''), ); - const underivedXPChainAddresses = secrets.xpubXP + + const extendedPublicKey = + 'extendedPublicKeys' in secrets + ? getExtendedPublicKey( + secrets.extendedPublicKeys, + AVALANCHE_BASE_DERIVATION_PATH, + 'secp256k1', + ) + : null; + + const underivedXPChainAddresses = extendedPublicKey ? ( await getAccountsWithActivity( - secrets.xpubXP, + extendedPublicKey.key, await this.networkService.getAvalanceProviderXP(), this.#getAddressesActivity, ) diff --git a/src/background/services/fireblocks/handlers/fireblocksUpdateApiCredentials.test.ts b/src/background/services/fireblocks/handlers/fireblocksUpdateApiCredentials.test.ts index d6bbb7236..e33720c36 100644 --- a/src/background/services/fireblocks/handlers/fireblocksUpdateApiCredentials.test.ts +++ b/src/background/services/fireblocks/handlers/fireblocksUpdateApiCredentials.test.ts @@ -37,6 +37,7 @@ describe('src/background/services/fireblocks/handlers/fireblocksUpdateApiCredent {} as any, {} as any, {} as any, + {} as any, ); const fireblocksServiceMock = new FireblocksService({} as any); diff --git a/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.test.ts b/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.test.ts index c9bff7104..d60d55c63 100644 --- a/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.test.ts +++ b/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.test.ts @@ -2,20 +2,62 @@ import { DerivationPath, getLedgerExtendedPublicKey, getPubKeyFromTransport, + getAddressDerivationPath, } from '@avalabs/core-wallets-sdk'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + AddressPublicKeyJson, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; import { SecretsService } from '../../secrets/SecretsService'; import { LedgerTransport } from '../LedgerTransport'; import { MigrateMissingPublicKeysFromLedgerHandler } from './migrateMissingPublicKeysFromLedger'; import { buildRpcCall } from '@src/tests/test-utils'; import { AccountsService } from '../../accounts/AccountsService'; import { Account } from '../../accounts/models'; +import { AddressPublicKey } from '../../secrets/AddressPublicKey'; +import { PubKeyType } from '../../wallet/models'; +import { buildExtendedPublicKey } from '../../secrets/utils'; jest.mock('../../secrets/SecretsService'); -jest.mock('@avalabs/core-wallets-sdk'); +jest.mock('@avalabs/core-wallets-sdk', () => ({ + ...jest.requireActual('@avalabs/core-wallets-sdk'), + getLedgerExtendedPublicKey: jest.fn(), + getPubKeyFromTransport: jest.fn(), +})); const WALLET_ID = 'wallet_id'; + +const mapLegacyPubKeys = (pubKeys: PubKeyType[]): AddressPublicKeyJson[] => + pubKeys + .flatMap(({ evm, xp }, index) => [ + AddressPublicKey.fromJSON({ + key: evm, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'EVM', + ), + }).toJSON(), + xp + ? AddressPublicKey.fromJSON({ + key: xp, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'AVM', + ), + }).toJSON() + : undefined, + ]) + .filter( + (key): key is AddressPublicKeyJson => key !== undefined, + ); + describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.ts', () => { const request = { id: '123', @@ -23,6 +65,7 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe } as any; const accountsService: jest.Mocked = { activeAccount: {} as unknown as Account, + getPrimaryAccountsByWalletId: jest.fn(), } as any; const secretsService = jest.mocked(new SecretsService({} as any)); @@ -77,9 +120,12 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe it('terminates early if there is nothing to update', async () => { secretsService.getAccountSecrets.mockResolvedValue({ secretType: SecretType.Ledger, - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey('xpubXP', AVALANCHE_BASE_DERIVATION_PATH), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, id: WALLET_ID, } as any); @@ -92,8 +138,11 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe it('updates the extended public key correctly', async () => { secretsService.getAccountSecrets.mockResolvedValue({ secretType: SecretType.Ledger, - xpub: 'xpub', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, id: WALLET_ID, } as any); @@ -103,7 +152,10 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe expect(result).toBe(true); expect(secretsService.updateSecrets).toHaveBeenCalledWith( { - xpubXP: 'xpubXP', + extendedPublicKeys: [ + buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey('xpubXP', AVALANCHE_BASE_DERIVATION_PATH), + ], }, WALLET_ID, ); @@ -112,10 +164,15 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe describe('Derivation path: Ledger Live', () => { it('terminates early if there is nothing to update', async () => { + accountsService.getPrimaryAccountsByWalletId.mockReturnValueOnce([ + { + index: 0, + }, + ] as any); secretsService.getAccountSecrets.mockResolvedValue({ secretType: SecretType.LedgerLive, - pubKeys: [], - derivationPath: DerivationPath.LedgerLive, + publicKeys: mapLegacyPubKeys([{ evm: 'evm', xp: 'xp' }]), + derivationPathSpec: DerivationPath.LedgerLive, id: WALLET_ID, } as any); @@ -125,6 +182,11 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe }); it('updates the pubkeys and throws if an error happened', async () => { + accountsService.getPrimaryAccountsByWalletId.mockReturnValueOnce([ + { index: 0 }, + { index: 1 }, + { index: 2 }, + ] as any); const pubKeys = [ { evm: 'evm', xp: 'xp' }, { evm: 'evm2', xp: '' }, @@ -132,8 +194,8 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe ]; secretsService.getAccountSecrets.mockResolvedValue({ secretType: SecretType.LedgerLive, - pubKeys, - derivationPath: DerivationPath.LedgerLive, + publicKeys: mapLegacyPubKeys(pubKeys), + derivationPathSpec: DerivationPath.LedgerLive, id: WALLET_ID, } as any); @@ -149,14 +211,16 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe expect(secretsService.updateSecrets).toHaveBeenCalledWith( { - pubKeys: [ - { evm: 'evm', xp: 'xp' }, - { - evm: 'evm2', - xp: '1234', - }, - { evm: 'evm3', xp: '' }, - ], + publicKeys: expect.arrayContaining( + mapLegacyPubKeys([ + { evm: 'evm', xp: 'xp' }, + { + evm: 'evm2', + xp: '1234', + }, + { evm: 'evm3', xp: '' }, + ]), + ), }, WALLET_ID, ); @@ -178,6 +242,11 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe }); it('updates the pubkeys correctly', async () => { + accountsService.getPrimaryAccountsByWalletId.mockReturnValueOnce([ + { index: 0 }, + { index: 1 }, + { index: 2 }, + ] as any); const pubKeys = [ { evm: 'evm', xp: 'xp' }, { evm: 'evm2', xp: '' }, @@ -186,8 +255,8 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe secretsService.getAccountSecrets.mockResolvedValue({ secretType: SecretType.LedgerLive, - pubKeys, - derivationPath: DerivationPath.LedgerLive, + publicKeys: mapLegacyPubKeys(pubKeys), + derivationPathSpec: DerivationPath.LedgerLive, id: WALLET_ID, } as any); jest @@ -200,14 +269,13 @@ describe('src/background/services/ledger/handlers/migrateMissingPublicKeysFromLe expect(result).toBe(true); expect(secretsService.updateSecrets).toHaveBeenCalledWith( { - pubKeys: [ - { evm: 'evm', xp: 'xp' }, - { - evm: 'evm2', - xp: '1234', - }, - { evm: 'evm3', xp: '5678' }, - ], + publicKeys: expect.arrayContaining( + mapLegacyPubKeys([ + { evm: 'evm', xp: 'xp' }, + { evm: 'evm2', xp: '1234' }, + { evm: 'evm3', xp: '5678' }, + ]), + ), }, WALLET_ID, ); diff --git a/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.ts b/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.ts index 14faea6ac..8aeef3281 100644 --- a/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.ts +++ b/src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger.ts @@ -3,15 +3,20 @@ import { DerivationPath, getLedgerExtendedPublicKey, getPubKeyFromTransport, + getAddressDerivationPath, } from '@avalabs/core-wallets-sdk'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; import { ExtensionRequestHandler } from '@src/background/connections/models'; import { injectable } from 'tsyringe'; -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + AddressPublicKeyJson, + SecretType, +} from '../../secrets/models'; import { SecretsService } from '../../secrets/SecretsService'; -import { PubKeyType } from '../../wallet/models'; import { LedgerService } from '../LedgerService'; import { AccountsService } from '../../accounts/AccountsService'; +import { getExtendedPublicKey, hasPublicKeyFor } from '../../secrets/utils'; type HandlerType = ExtensionRequestHandler< ExtensionRequest.LEDGER_MIGRATE_MISSING_PUBKEYS, @@ -60,7 +65,13 @@ export class MigrateMissingPublicKeysFromLedgerHandler implements HandlerType { if (secrets.secretType === SecretType.Ledger) { // nothing to update, exit early - if (secrets.xpubXP) { + if ( + getExtendedPublicKey( + secrets.extendedPublicKeys, + AVALANCHE_BASE_DERIVATION_PATH, + 'secp256k1', + ) + ) { return { ...request, result: true, @@ -73,14 +84,41 @@ export class MigrateMissingPublicKeysFromLedgerHandler implements HandlerType { Avalanche.LedgerWallet.getAccountPath('X'), ); - await this.secretsService.updateSecrets({ xpubXP }, walletId); - } else if (secrets.secretType === SecretType.LedgerLive) { - const hasMissingXPPublicKey = (secrets.pubKeys ?? []).some( - (pubKey) => !pubKey.xp, + await this.secretsService.updateSecrets( + { + extendedPublicKeys: [ + ...secrets.extendedPublicKeys, + { + type: 'extended-pubkey', + curve: 'secp256k1', + derivationPath: AVALANCHE_BASE_DERIVATION_PATH, + key: xpubXP, + }, + ], + }, + walletId, ); + } else if (secrets.secretType === SecretType.LedgerLive) { + const accounts = + await this.accountsService.getPrimaryAccountsByWalletId(secrets.id); + + const missingDerivationPaths: [number, string][] = []; + + for (let i = 0; i < accounts.length; i++) { + const account = accounts[i]!; + const avmDerivationPath = getAddressDerivationPath( + account.index, + DerivationPath.LedgerLive, + 'AVM', + ); + + if (!hasPublicKeyFor(secrets, avmDerivationPath, 'secp256k1')) { + missingDerivationPaths.push([account.index, avmDerivationPath]); + } + } // nothing to migrate, exit early - if (!hasMissingXPPublicKey) { + if (!missingDerivationPaths.length) { return { ...request, result: true, @@ -88,39 +126,37 @@ export class MigrateMissingPublicKeysFromLedgerHandler implements HandlerType { } const migrationResult: { - updatedPubKeys: PubKeyType[]; + newPubKeys: AddressPublicKeyJson[]; hasError: boolean; } = { - updatedPubKeys: [], + newPubKeys: [], hasError: false, }; - for (const [index, pubKey] of (secrets.pubKeys ?? []).entries()) { - if (!pubKey.xp) { - try { - const addressPublicKeyXP = await getPubKeyFromTransport( - transport, - index, - DerivationPath.LedgerLive, - 'AVM', - ); - - migrationResult.updatedPubKeys.push({ - ...pubKey, - xp: addressPublicKeyXP.toString('hex'), - }); - } catch (_err) { - migrationResult.updatedPubKeys.push(pubKey); - migrationResult.hasError = true; - } - } else { - migrationResult.updatedPubKeys.push(pubKey); + for (const [accountIndex, derivationPath] of missingDerivationPaths) { + try { + const addressPublicKeyXP = await getPubKeyFromTransport( + transport, + accountIndex, + DerivationPath.LedgerLive, + 'AVM', + ); + + migrationResult.newPubKeys.push({ + curve: 'secp256k1', + derivationPath, + key: addressPublicKeyXP.toString('hex'), + type: 'address-pubkey', + }); + } catch (_err) { + migrationResult.hasError = true; + break; } } await this.secretsService.updateSecrets( { - pubKeys: migrationResult.updatedPubKeys, + publicKeys: [...secrets.publicKeys, ...migrationResult.newPubKeys], }, walletId, ); diff --git a/src/background/services/onboarding/handlers/keystoneOnboardingHandler.test.ts b/src/background/services/onboarding/handlers/keystoneOnboardingHandler.test.ts index d06068713..8728b2397 100644 --- a/src/background/services/onboarding/handlers/keystoneOnboardingHandler.test.ts +++ b/src/background/services/onboarding/handlers/keystoneOnboardingHandler.test.ts @@ -1,4 +1,4 @@ -import { SecretType } from '../../secrets/models'; +import { EVM_BASE_DERIVATION_PATH, SecretType } from '../../secrets/models'; import { Avalanche, DerivationPath, @@ -16,6 +16,7 @@ import { NetworkService } from '../../network/NetworkService'; import { KeystoneOnboardingHandler } from './keystoneOnboardingHandler'; import { buildRpcCall } from '@src/tests/test-utils'; import { addXPChainToFavoriteIfNeeded } from '../utils/addXPChainsToFavoriteIfNeeded'; +import { buildExtendedPublicKey } from '../../secrets/utils'; jest.mock('../utils/addXPChainsToFavoriteIfNeeded'); @@ -128,9 +129,12 @@ describe('src/background/services/onboarding/handlers/keystoneOnboardingHandler. 'password', ); expect(walletServiceMock.init).toHaveBeenCalledWith({ - xpub: 'xpub', + extendedPublicKeys: [ + buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + ], + publicKeys: [], masterFingerprint: 'masterFingerprint', - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, secretType: SecretType.Keystone, name: undefined, }); diff --git a/src/background/services/onboarding/handlers/keystoneOnboardingHandler.ts b/src/background/services/onboarding/handlers/keystoneOnboardingHandler.ts index b94a7160f..9bcc91c9c 100644 --- a/src/background/services/onboarding/handlers/keystoneOnboardingHandler.ts +++ b/src/background/services/onboarding/handlers/keystoneOnboardingHandler.ts @@ -1,5 +1,5 @@ import { DerivationPath } from '@avalabs/core-wallets-sdk'; -import { SecretType } from '../../secrets/models'; +import { EVM_BASE_DERIVATION_PATH, SecretType } from '../../secrets/models'; import { SettingsService } from '../../settings/SettingsService'; import { StorageService } from '../../storage/StorageService'; import { AnalyticsService } from '../../analytics/AnalyticsService'; @@ -13,6 +13,7 @@ import { ExtensionRequestHandler } from '@src/background/connections/models'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; import { finalizeOnboarding } from '../finalizeOnboarding'; import { startOnboarding } from '../startOnboarding'; +import { buildExtendedPublicKey } from '../../secrets/utils'; type HandlerType = ExtensionRequestHandler< ExtensionRequest.KEYSTONE_ONBOARDING_SUBMIT, @@ -57,9 +58,12 @@ export class KeystoneOnboardingHandler implements HandlerType { const walletId = await this.walletService.init({ secretType: SecretType.Keystone, - xpub, + extendedPublicKeys: [ + buildExtendedPublicKey(xpub, EVM_BASE_DERIVATION_PATH), + ], + publicKeys: [], masterFingerprint, - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, name: walletName, }); diff --git a/src/background/services/onboarding/handlers/ledgerOnboardingHandler.test.ts b/src/background/services/onboarding/handlers/ledgerOnboardingHandler.test.ts index 4e3a16923..3db2687c8 100644 --- a/src/background/services/onboarding/handlers/ledgerOnboardingHandler.test.ts +++ b/src/background/services/onboarding/handlers/ledgerOnboardingHandler.test.ts @@ -2,6 +2,7 @@ import { Avalanche, DerivationPath, getXpubFromMnemonic, + getAddressDerivationPath, } from '@avalabs/core-wallets-sdk'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; import { AccountsService } from '../../accounts/AccountsService'; @@ -13,9 +14,14 @@ import { StorageService } from '../../storage/StorageService'; import { WalletService } from '../../wallet/WalletService'; import { OnboardingService } from '../OnboardingService'; import { LedgerOnboardingHandler } from './ledgerOnboardingHandler'; -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; import { buildRpcCall } from '@src/tests/test-utils'; import { addXPChainToFavoriteIfNeeded } from '../utils/addXPChainsToFavoriteIfNeeded'; +import { buildExtendedPublicKey } from '../../secrets/utils'; jest.mock('../utils/addXPChainsToFavoriteIfNeeded'); @@ -129,9 +135,12 @@ describe('src/background/services/onboarding/handlers/ledgerOnboardingHandler.ts ); expect(walletServiceMock.init).toHaveBeenCalledWith({ mnemonic: undefined, - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey('xpubXP', AVALANCHE_BASE_DERIVATION_PATH), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, secretType: SecretType.Ledger, name: 'wallet-name', }); @@ -150,7 +159,10 @@ describe('src/background/services/onboarding/handlers/ledgerOnboardingHandler.ts const handler = getHandler(); const request = getRequest([ { - pubKeys: ['pubkey1', 'pubkey2', 'pubkey3'], + pubKeys: [ + { evm: 'evm1', xp: 'xp1' }, + { evm: 'evm2', xp: 'xp2' }, + ], password: 'password', walletName: 'wallet-name', analyticsConsent: false, @@ -171,8 +183,49 @@ describe('src/background/services/onboarding/handlers/ledgerOnboardingHandler.ts 'password', ); expect(walletServiceMock.init).toHaveBeenCalledWith({ - pubKeys: ['pubkey1', 'pubkey2', 'pubkey3'], - derivationPath: DerivationPath.LedgerLive, + publicKeys: [ + { + curve: 'secp256k1', + key: 'evm1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.LedgerLive, + 'EVM', + ), + type: 'address-pubkey', + }, + { + curve: 'secp256k1', + key: 'xp1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.LedgerLive, + 'AVM', + ), + type: 'address-pubkey', + }, + { + curve: 'secp256k1', + key: 'evm2', + derivationPath: getAddressDerivationPath( + 1, + DerivationPath.LedgerLive, + 'EVM', + ), + type: 'address-pubkey', + }, + { + curve: 'secp256k1', + key: 'xp2', + derivationPath: getAddressDerivationPath( + 1, + DerivationPath.LedgerLive, + 'AVM', + ), + type: 'address-pubkey', + }, + ], + derivationPathSpec: DerivationPath.LedgerLive, secretType: SecretType.LedgerLive, name: 'wallet-name', }); diff --git a/src/background/services/onboarding/handlers/ledgerOnboardingHandler.ts b/src/background/services/onboarding/handlers/ledgerOnboardingHandler.ts index f8b7f563a..b85d76745 100644 --- a/src/background/services/onboarding/handlers/ledgerOnboardingHandler.ts +++ b/src/background/services/onboarding/handlers/ledgerOnboardingHandler.ts @@ -1,5 +1,13 @@ -import { DerivationPath } from '@avalabs/core-wallets-sdk'; -import { SecretType } from '../../secrets/models'; +import { + DerivationPath, + getAddressDerivationPath, +} from '@avalabs/core-wallets-sdk'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + AddressPublicKeyJson, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; import { SettingsService } from '../../settings/SettingsService'; import { StorageService } from '../../storage/StorageService'; import { AnalyticsService } from '../../analytics/AnalyticsService'; @@ -14,6 +22,7 @@ import { ExtensionRequest } from '@src/background/connections/extensionConnectio import { PubKeyType } from '../../wallet/models'; import { finalizeOnboarding } from '../finalizeOnboarding'; import { startOnboarding } from '../startOnboarding'; +import { buildExtendedPublicKey } from '../../secrets/utils'; type HandlerType = ExtensionRequestHandler< ExtensionRequest.LEDGER_ONBOARDING_SUBMIT, @@ -77,18 +86,49 @@ export class LedgerOnboardingHandler implements HandlerType { if (xpub && xpubXP) { walletId = await this.walletService.init({ secretType: SecretType.Ledger, - xpub, - xpubXP, - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey(xpub, EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey(xpubXP, AVALANCHE_BASE_DERIVATION_PATH), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, name: walletName, }); } if (pubKeys?.length) { + const publicKeys: AddressPublicKeyJson[] = []; + + for (const [index, pubKey] of pubKeys.entries()) { + publicKeys.push({ + curve: 'secp256k1', + key: pubKey.evm, + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'EVM', + ), + type: 'address-pubkey', + }); + + if (pubKey.xp) { + publicKeys.push({ + curve: 'secp256k1', + key: pubKey.xp, + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'AVM', + ), + type: 'address-pubkey', + }); + } + } + walletId = await this.walletService.init({ secretType: SecretType.LedgerLive, - pubKeys, - derivationPath: DerivationPath.LedgerLive, + publicKeys, + derivationPathSpec: DerivationPath.LedgerLive, name: walletName, }); } diff --git a/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.test.ts b/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.test.ts index ac5066519..8c45fd6e7 100644 --- a/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.test.ts +++ b/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.test.ts @@ -1,4 +1,8 @@ -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; import { Avalanche, DerivationPath, @@ -16,6 +20,7 @@ import { SettingsService } from '../../settings/SettingsService'; import { NetworkService } from '../../network/NetworkService'; import { buildRpcCall } from '@src/tests/test-utils'; import { addXPChainToFavoriteIfNeeded } from '../utils/addXPChainsToFavoriteIfNeeded'; +import { buildExtendedPublicKey } from '../../secrets/utils'; jest.mock('../utils/addXPChainsToFavoriteIfNeeded'); @@ -167,9 +172,15 @@ describe('src/background/services/onboarding/handlers/mnemonicOnboardingHandler. ); expect(walletServiceMock.init).toHaveBeenCalledWith({ mnemonic: 'mnemonic', - xpub: 'xpubFromMnemonic', - xpubXP: 'xpubFromMnemonicXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey('xpubFromMnemonic', EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey( + 'xpubFromMnemonicXP', + AVALANCHE_BASE_DERIVATION_PATH, + ), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, secretType: SecretType.Mnemonic, name: undefined, }); diff --git a/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.ts b/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.ts index 95d1d4fa5..e09814d68 100644 --- a/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.ts +++ b/src/background/services/onboarding/handlers/mnemonicOnboardingHandler.ts @@ -2,7 +2,11 @@ import { ExtensionRequest } from '@src/background/connections/extensionConnectio import { ExtensionRequestHandler } from '@src/background/connections/models'; import { StorageService } from '../../storage/StorageService'; import { SettingsService } from '../../settings/SettingsService'; -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; import { WalletService } from '../../wallet/WalletService'; import { AnalyticsService } from '../../analytics/AnalyticsService'; import { @@ -17,6 +21,7 @@ import { NetworkService } from '../../network/NetworkService'; import { injectable } from 'tsyringe'; import { finalizeOnboarding } from '../finalizeOnboarding'; import { startOnboarding } from '../startOnboarding'; +import { buildExtendedPublicKey } from '../../secrets/utils'; type HandlerType = ExtensionRequestHandler< ExtensionRequest.MNEMONIC_ONBOARDING_SUBMIT, @@ -69,9 +74,12 @@ export class MnemonicOnboardingHandler implements HandlerType { const walletId = await this.walletService.init({ secretType: SecretType.Mnemonic, mnemonic, - xpub, - xpubXP, - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey(xpub, EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey(xpubXP, AVALANCHE_BASE_DERIVATION_PATH), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, name: walletName, }); diff --git a/src/background/services/onboarding/handlers/seedlessOnboardingHandler.test.ts b/src/background/services/onboarding/handlers/seedlessOnboardingHandler.test.ts index b7484a2b0..47b1f800c 100644 --- a/src/background/services/onboarding/handlers/seedlessOnboardingHandler.test.ts +++ b/src/background/services/onboarding/handlers/seedlessOnboardingHandler.test.ts @@ -127,14 +127,26 @@ describe('src/background/services/onboarding/handlers/seedlessOnboardingHandler. .mocked(secretsServiceMock.getWalletAccountsSecretsById) .mockResolvedValueOnce({ secretType: SecretType.Seedless, - pubKeys: [ + publicKeys: [ { - evm: 'evm', - xp: 'xp', + key: 'evm', + derivationPath: `m/44'/60'/0'/0/0`, + curve: 'secp256k1', }, { - evm: 'evm2', - xp: 'xp2', + key: 'xp', + derivationPath: `m/44'/9000'/0'/0/0`, + curve: 'secp256k1', + }, + { + key: 'evm', + derivationPath: `m/44'/60'/0'/0/1`, + curve: 'secp256k1', + }, + { + key: 'xp', + derivationPath: `m/44'/9000'/0'/0/1`, + curve: 'secp256k1', }, ], } as any); @@ -167,8 +179,8 @@ describe('src/background/services/onboarding/handlers/seedlessOnboardingHandler. seedlessSignerToken: undefined, authProvider: SeedlessAuthProvider.Google, userId: '123', - pubKeys: [], - derivationPath: DerivationPath.BIP44, + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, secretType: SecretType.Seedless, name: 'wallet-name', }); diff --git a/src/background/services/onboarding/handlers/seedlessOnboardingHandler.ts b/src/background/services/onboarding/handlers/seedlessOnboardingHandler.ts index 31e549b94..9278c75c3 100644 --- a/src/background/services/onboarding/handlers/seedlessOnboardingHandler.ts +++ b/src/background/services/onboarding/handlers/seedlessOnboardingHandler.ts @@ -78,11 +78,11 @@ export class SeedlessOnboardingHandler implements HandlerType { const walletId = await this.walletService.init({ secretType: SecretType.Seedless, - pubKeys: (await seedlessWallet.getPublicKeys()) ?? [], + publicKeys: (await seedlessWallet.getPublicKeys()) ?? [], seedlessSignerToken: await memorySessionStorage.retrieve(), userId, authProvider, - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, name: walletName, }); @@ -96,9 +96,15 @@ export class SeedlessOnboardingHandler implements HandlerType { const secrets = await this.secretsService.getWalletAccountsSecretsById(walletId); if (secrets?.secretType === SecretType.Seedless) { + // To get the number of accounts, we find the number of + // unique public keys with the most common derivation path (EVM). + const numberOfAccounts = secrets.publicKeys.filter((pubKey) => + pubKey.derivationPath.startsWith("m/44'/60'/"), + ).length; + // Adding accounts cannot be parallelized, they need to be added one-by-one. // Otherwise race conditions occur and addresses get mixed up. - for (let i = 0; i < secrets.pubKeys.length; i++) { + for (let i = 0; i < numberOfAccounts; i++) { await this.accountsService.addPrimaryAccount({ walletId, }); diff --git a/src/background/services/secrets/AddressPublicKey.ts b/src/background/services/secrets/AddressPublicKey.ts new file mode 100644 index 000000000..28d6fe195 --- /dev/null +++ b/src/background/services/secrets/AddressPublicKey.ts @@ -0,0 +1,170 @@ +import { hex } from '@scure/base'; +import { mnemonicToSeed } from 'bip39'; +import { fromBase58, fromSeed } from 'bip32'; +import { ed25519 } from '@noble/curves/ed25519'; +import slip10 from 'micro-key-producer/slip10.js'; +import { BtcWalletPolicyDetails } from '@avalabs/vm-module-types'; +import { getPublicKeyFromPrivateKey } from '@avalabs/core-wallets-sdk'; + +import { SecretsError } from '@src/utils/errors'; +import { assertPresent } from '@src/utils/assertions'; + +import { + AddressPublicKeyJson, + Curve, + ExtendedPublicKey, + ImportedAccountSecrets, + PrimaryWalletSecrets, + SecretType, +} from './models'; +import { assertDerivationPath, getExtendedPublicKeyFor } from './utils'; + +export class AddressPublicKey { + private readonly type = 'address-pubkey'; + + constructor( + public key: string, + public curve: Curve, + public derivationPath: HasDerivationPath extends true ? string : null, + public btcWalletPolicyDetails?: BtcWalletPolicyDetails, + ) {} + + toJSON(): AddressPublicKeyJson { + return { + type: this.type, + curve: this.curve, + derivationPath: this.derivationPath, + key: this.key, + ...(this.btcWalletPolicyDetails && { + btcWalletPolicyDetails: this.btcWalletPolicyDetails, + }), + }; + } + + static fromJSON(json: Omit): AddressPublicKey { + return new AddressPublicKey( + json.key, + json.curve, + json.derivationPath, + json.btcWalletPolicyDetails, + ); + } + + static async fromSecrets( + secrets: PrimaryWalletSecrets | ImportedAccountSecrets, + curve: Curve, + derivationPath?: string, + ): Promise> { + if (secrets.secretType === SecretType.Mnemonic) { + assertDerivationPath(derivationPath); + return AddressPublicKey.fromSeedphrase( + secrets.mnemonic, + curve, + derivationPath, + ); + } + + if (secrets.secretType === SecretType.Ledger) { + assertDerivationPath(derivationPath); + return AddressPublicKey.fromExtendedPublicKeys( + secrets.extendedPublicKeys, + curve, + derivationPath, + ); + } + + if (secrets.secretType === SecretType.Seedless) { + assertDerivationPath(derivationPath); + + const pubKeyJson = secrets.publicKeys.find( + (publicKey) => + publicKey.curve === curve && + publicKey.derivationPath === derivationPath, + ); + + assertPresent(pubKeyJson, SecretsError.PublicKeyNotFound); + + return AddressPublicKey.fromJSON(pubKeyJson); + } + + if (secrets.secretType === SecretType.PrivateKey) { + return AddressPublicKey.fromPrivateKey(secrets.secret, curve); + } + + throw new Error('Not implemented yet'); + } + + static fromExtendedPublicKeys( + xpubs: ExtendedPublicKey[], + curve: Curve, + derivationPath: string, + ): AddressPublicKey { + const matchingXpub = getExtendedPublicKeyFor(xpubs, derivationPath, curve); + + if (!matchingXpub) { + throw new Error( + 'No matching extended public key found for derivation path: ' + + derivationPath, + ); + } + const pathSuffix = derivationPath.slice( + matchingXpub.derivationPath.length + 1, // Add one to account for the trailing slash from the lookup + ); + const node = fromBase58(matchingXpub.key).derivePath(pathSuffix); + const key = hex.encode(node.publicKey); + + return new AddressPublicKey( + key, + curve, + derivationPath, + matchingXpub.btcWalletPolicyDetails, + ); + } + + static fromPrivateKey( + privateKey: string, + curve: Curve, + ): AddressPublicKey { + let key: string; + + switch (curve) { + case 'ed25519': + key = hex.encode(ed25519.getPublicKey(privateKey)); + break; + + case 'secp256k1': + key = hex.encode(getPublicKeyFromPrivateKey(privateKey)); + break; + } + + return new AddressPublicKey(key, curve, null); + } + + static async fromSeedphrase( + seedphrase: string, + curve: Curve, + derivationPath: string, + ): Promise { + const seed = await mnemonicToSeed(seedphrase); + let key: string; + + switch (curve) { + case 'secp256k1': { + const seedNode = fromSeed(seed); + key = hex.encode(seedNode.derivePath(derivationPath).publicKey); + break; + } + + case 'ed25519': { + const hdKey = slip10.fromMasterSeed(seed); + key = hex.encode(hdKey.derive(derivationPath).publicKey); + break; + } + + default: + throw new Error('Unsupported curve'); + } + + return new AddressPublicKey(key, curve, derivationPath); + } +} diff --git a/src/background/services/secrets/AddressResolver.ts b/src/background/services/secrets/AddressResolver.ts new file mode 100644 index 000000000..56650a106 --- /dev/null +++ b/src/background/services/secrets/AddressResolver.ts @@ -0,0 +1,157 @@ +import { singleton } from 'tsyringe'; +import { Module, NetworkVMType } from '@avalabs/vm-module-types'; +import type { ModuleManager } from '@src/background/vmModules/ModuleManager'; + +import { isDevnet } from '@src/utils/isDevnet'; +import { CommonError, SecretsError } from '@src/utils/errors'; +import { assertPresent } from '@src/utils/assertions'; + +import { NetworkWithCaipId } from '../network/models'; +import { NetworkService } from '../network/NetworkService'; +import { emptyAddresses, emptyDerivationPaths } from './utils'; +import { DerivationPath } from '@avalabs/core-wallets-sdk'; +import { SecretsService } from './SecretsService'; +import { DerivationPathsMap, SecretType } from './models'; + +@singleton() +export class AddressResolver { + #moduleManager?: ModuleManager; + + constructor( + private readonly networkService: NetworkService, + private readonly secretsService: SecretsService, + ) {} + + init(moduleManager: ModuleManager) { + this.#moduleManager = moduleManager; + } + + async #getNetworksForAddressDerivation(): Promise { + const allNetworksForEnv = Object.values( + await this.networkService.activeNetworks.promisify(), + ); + + /** + * In some instances (like X- and P-Chain), we may get two conflicting networks + * in the test environment (e.g. both Fuji P-Chain, and Devnet P-Chain). + * + * The two variants would result in conflicting addresses, so we need to filter + * one of them out, based on whichever is active. + * + * TODO: find a nicer way to do it. Ideas: + * 1) have a 3rd environment (mainnet / testnet / devnet) + * 2) have separate NetworkVMType for testnets & devnets + * 3) in the AccountService, do not segregate addresses by NetworkVMType, + * but rather by CAIP-2 ids (whole ID or just namespace and then choose the more specific one) + */ + const isDevnetOnTheList = allNetworksForEnv.some(isDevnet); + + if (!isDevnetOnTheList) { + return allNetworksForEnv; + } + + const isDevnetActive = this.networkService.uiActiveNetwork + ? isDevnet(this.networkService.uiActiveNetwork) + : false; + + return allNetworksForEnv.filter((network) => { + if ( + network.vmName !== NetworkVMType.AVM && + network.vmName !== NetworkVMType.PVM + ) { + return true; + } + + return isDevnetActive ? isDevnet(network) : true; + }); + } + + async getDerivationPaths( + accountIndex: number, + derivationPathType: DerivationPath, + ): Promise { + assertPresent(this.#moduleManager, CommonError.ModuleManagerNotSet); + + const derivationPaths = emptyDerivationPaths(); + + const activeNetworks = await this.#getNetworksForAddressDerivation(); + const modules = new Set(); + + for (const network of activeNetworks) { + const module = await this.#moduleManager.loadModuleByNetwork(network); + if (module && !modules.has(module)) { + modules.add(module); + } + } + + for (const module of modules) { + const modulePaths = await module.buildDerivationPath({ + accountIndex, + derivationPathType, + }); + + for (const [vmType, address] of Object.entries(modulePaths)) { + derivationPaths[vmType] = address; + } + } + + for (const [vmType, path] of Object.entries(derivationPaths)) { + assertPresent(path, SecretsError.DerivationPathMissing, vmType); + } + + return derivationPaths; + } + + async getAddressesForSecretId( + secretId: string, + accountIndex?: number, + derivationPathType?: DerivationPath, + ): Promise | never> { + assertPresent(this.#moduleManager, CommonError.ModuleManagerNotSet); + + const secrets = await this.secretsService.getSecretsById(secretId); + const addresses = emptyAddresses(); + + if (secrets.secretType === SecretType.Fireblocks) { + return { + ...addresses, + [NetworkVMType.EVM]: secrets.addresses.addressC, + [NetworkVMType.BITCOIN]: secrets.addresses.addressBTC ?? '', + }; + } else if (secrets.secretType === SecretType.WalletConnect) { + return { + ...addresses, + [NetworkVMType.EVM]: secrets.addresses.addressC, + [NetworkVMType.AVM]: secrets.addresses.addressAVM ?? '', + [NetworkVMType.PVM]: secrets.addresses.addressPVM ?? '', + [NetworkVMType.BITCOIN]: secrets.addresses.addressBTC ?? '', + [NetworkVMType.CoreEth]: secrets.addresses.addressCoreEth ?? '', + }; + } + + const activeNetworks = await this.#getNetworksForAddressDerivation(); + const modules = new Map(); + + for (const network of activeNetworks) { + const module = await this.#moduleManager.loadModuleByNetwork(network); + if (module && !modules.has(module)) { + modules.set(module, network); + } + } + + for (const [module, network] of modules.entries()) { + const moduleAddresses = await module.deriveAddress({ + accountIndex, + network, + secretId, + derivationPathType, + }); + + for (const [vmType, address] of Object.entries(moduleAddresses)) { + addresses[vmType] = address; + } + } + + return addresses; + } +} diff --git a/src/background/services/secrets/SecretsService.test.ts b/src/background/services/secrets/SecretsService.test.ts index e65200de7..8650ea769 100644 --- a/src/background/services/secrets/SecretsService.test.ts +++ b/src/background/services/secrets/SecretsService.test.ts @@ -2,11 +2,6 @@ import { Avalanche, DerivationPath, getPubKeyFromTransport, - getAddressFromXPub, - getBech32AddressFromXPub, - getEvmAddressFromPubKey, - getBtcAddressFromPubKey, - getPublicKeyFromPrivateKey, } from '@avalabs/core-wallets-sdk'; import { CallbackManager } from '@src/background/runtime/CallbackManager'; import { @@ -15,23 +10,27 @@ import { ImportType, PrimaryAccount, } from '../accounts/models'; -import { NetworkService } from '../network/NetworkService'; import { StorageService } from '../storage/StorageService'; -import { PubKeyType, WALLET_STORAGE_KEY, WalletEvents } from '../wallet/models'; -import { SecretType } from './models'; +import { WALLET_STORAGE_KEY, WalletEvents } from '../wallet/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from './models'; import { SecretsService } from './SecretsService'; import { WalletConnectService } from '../walletConnect/WalletConnectService'; import { LedgerService } from '../ledger/LedgerService'; import { LedgerTransport } from '../ledger/LedgerTransport'; import { SeedlessWallet } from '../seedless/SeedlessWallet'; import { SeedlessTokenStorage } from '../seedless/SeedlessTokenStorage'; -import { NetworkVMType } from '@avalabs/core-chains-sdk'; -import { networks } from 'bitcoinjs-lib'; -import { getAccountPrivateKeyFromMnemonic } from './utils/getAccountPrivateKeyFromMnemonic'; -import { getAddressForHvm } from './utils/getAddressForHvm'; +import * as utils from './utils'; +import { expectToThrowErroCode } from '@src/tests/test-utils'; +import { LedgerError } from '@src/utils/errors'; +import { AddressResolver } from './AddressResolver'; +import { mapVMAddresses } from '../accounts/utils/mapVMAddresses'; +import { NetworkVMType } from '@avalabs/vm-module-types'; jest.mock('../storage/StorageService'); -jest.mock('../network/NetworkService'); jest.mock('../walletConnect/WalletConnectService'); jest.mock('@avalabs/core-wallets-sdk'); jest.mock('../seedless/SeedlessWallet'); @@ -72,9 +71,6 @@ const WALLET_ID = 'wallet-id'; const ACTIVE_WALLET_ID = 'active-wallet-id'; describe('src/background/services/secrets/SecretsService.ts', () => { const storageService = jest.mocked(new StorageService({} as CallbackManager)); - const networkService = jest.mocked( - new NetworkService(storageService, {} as any), - ); const activeAccount = { type: AccountType.PRIMARY, walletId: ACTIVE_WALLET_ID, @@ -100,10 +96,6 @@ describe('src/background/services/secrets/SecretsService.ts', () => { getDefaultFujiProvider: getDefaultFujiProviderMock, } as any; - networkService.getAvalanceProviderXP.mockReturnValue( - getDefaultFujiProviderMock(), - ); - secretsService = new SecretsService(storageService); }); @@ -116,14 +108,14 @@ describe('src/background/services/secrets/SecretsService.ts', () => { name: 'Recovery Phrase 01', xpub: 'xpub', xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, id: 'mnemonic-wallet', }, { xpub: 'xpub', xpubXP: 'xpubXP', name: 'Ledger 01', - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, id: 'ledger-wallet', secretType: SecretType.Ledger, }, @@ -147,9 +139,22 @@ describe('src/background/services/secrets/SecretsService.ts', () => { { secretType: SecretType.Mnemonic, mnemonic: 'mnemonic', - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + { + key: 'xpub', + derivationPath: EVM_BASE_DERIVATION_PATH, + curve: 'secp256k1', + type: 'extended-pubkey', + }, + { + key: 'xpubXP', + derivationPath: AVALANCHE_BASE_DERIVATION_PATH, + curve: 'secp256k1', + type: 'extended-pubkey', + }, + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, id: ACTIVE_WALLET_ID, }, additionalWalletData, @@ -159,9 +164,22 @@ describe('src/background/services/secrets/SecretsService.ts', () => { { secretType: SecretType.Mnemonic, mnemonic: 'mnemonic', - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + { + key: 'xpub', + derivationPath: EVM_BASE_DERIVATION_PATH, + curve: 'secp256k1', + type: 'extended-pubkey', + }, + { + key: 'xpubXP', + derivationPath: AVALANCHE_BASE_DERIVATION_PATH, + curve: 'secp256k1', + type: 'extended-pubkey', + }, + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, id: ACTIVE_WALLET_ID, }, ]; @@ -181,8 +199,16 @@ describe('src/background/services/secrets/SecretsService.ts', () => { { secretType: SecretType.Keystone, masterFingerprint: 'masterFingerprint', - xpub: 'xpub', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + { + key: 'xpub', + derivationPath: EVM_BASE_DERIVATION_PATH, + curve: 'secp256k1', + type: 'extended-pubkey', + }, + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, id: ACTIVE_WALLET_ID, ...additionalData, }, @@ -197,9 +223,21 @@ describe('src/background/services/secrets/SecretsService.ts', () => { const data = { wallets: [ { - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + { + key: 'xpub', + derivationPath: EVM_BASE_DERIVATION_PATH, + curve: 'secp256k1', + type: 'extended-pubkey', + }, + { + key: 'xpubXP', + derivationPath: AVALANCHE_BASE_DERIVATION_PATH, + curve: 'secp256k1', + type: 'extended-pubkey', + }, + ], + derivationPathSpec: DerivationPath.BIP44, id: ACTIVE_WALLET_ID, secretType: SecretType.Ledger, ...additionalData, @@ -215,8 +253,11 @@ describe('src/background/services/secrets/SecretsService.ts', () => { const data = { wallets: [ { - derivationPath: DerivationPath.LedgerLive, - pubKeys: [{ evm: 'evm', xp: 'xp' }], + derivationPathSpec: DerivationPath.LedgerLive, + publicKeys: [ + { type: 'address-pubkey', curve: 'secp256k1', key: 'evm' }, + { type: 'address-pubkey', curve: 'secp256k1', key: 'xp' }, + ], id: ACTIVE_WALLET_ID, secretType: SecretType.LedgerLive, ...additionalData, @@ -234,8 +275,8 @@ describe('src/background/services/secrets/SecretsService.ts', () => { ) => { const data = { secretType: SecretType.Seedless, - derivationPath: DerivationPath.BIP44, - pubKeys: [{ evm: 'evm', xp: 'xp' }], + derivationPathSpec: DerivationPath.BIP44, + publicKeys: [], walletId: WALLET_ID, name: 'seedles', userId: '123', @@ -256,7 +297,7 @@ describe('src/background/services/secrets/SecretsService.ts', () => { secretsService.getPrimaryWalletsDetails = jest.fn().mockResolvedValue([ { type: data.secretType, - derivationPath: data.derivationPath, + derivationPathSpec: data.derivationPath, id: data.walletId, name: data.name, authProvider: data.authProvider, @@ -330,7 +371,7 @@ describe('src/background/services/secrets/SecretsService.ts', () => { id: wallet.id, name: wallet.name, type: wallet.secretType, - derivationPath: wallet.derivationPath, + derivationPath: wallet.derivationPathSpec, })), ); }); @@ -359,9 +400,9 @@ describe('src/background/services/secrets/SecretsService.ts', () => { await secretsService.addSecrets({ mnemonic: 'mnemonic', secretType: SecretType.Mnemonic, - derivationPath: DerivationPath.BIP44, - xpub: 'xpib', - xpubXP: 'xpubXP', + derivationPathSpec: DerivationPath.BIP44, + extendedPublicKeys: [{}, {}] as any, + publicKeys: [], name: 'walletName', }); expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { @@ -372,9 +413,9 @@ describe('src/background/services/secrets/SecretsService.ts', () => { id: uuid, mnemonic: 'mnemonic', secretType: SecretType.Mnemonic, - derivationPath: DerivationPath.BIP44, - xpub: 'xpib', - xpubXP: 'xpubXP', + derivationPathSpec: DerivationPath.BIP44, + extendedPublicKeys: [{}, {}], + publicKeys: [], name: 'walletName', }, ], @@ -391,16 +432,16 @@ describe('src/background/services/secrets/SecretsService.ts', () => { }, ]; const existingSecrets = { - xpub: 'xpub', + extendedPublicKeys: [{ type: 'extended-pubkey', key: '1234' }], wallets: existingWallets, }; storageService.load.mockResolvedValue(existingSecrets); await secretsService.addSecrets({ mnemonic: 'mnemonic', secretType: SecretType.Mnemonic, - derivationPath: DerivationPath.BIP44, - xpub: 'xpib', - xpubXP: 'xpubXP', + derivationPathSpec: DerivationPath.BIP44, + extendedPublicKeys: [{ type: 'extended-pubkey', key: '5678' }] as any, + publicKeys: [{ type: 'address-pubkey', key: '1234' }] as any, }); expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { ...existingSecrets, @@ -410,9 +451,9 @@ describe('src/background/services/secrets/SecretsService.ts', () => { id: uuid, mnemonic: 'mnemonic', secretType: SecretType.Mnemonic, - derivationPath: DerivationPath.BIP44, - xpub: 'xpib', - xpubXP: 'xpubXP', + derivationPathSpec: DerivationPath.BIP44, + extendedPublicKeys: [{ type: 'extended-pubkey', key: '5678' }], + publicKeys: [{ type: 'address-pubkey', key: '1234' }], name: 'Recovery Phrase 01', }, ], @@ -434,9 +475,9 @@ describe('src/background/services/secrets/SecretsService.ts', () => { storageService.load.mockResolvedValue(existingSecrets); await secretsService.addSecrets({ secretType: SecretType.Ledger, - derivationPath: DerivationPath.BIP44, - xpub: 'xpub', - xpubXP: 'xpubXP', + derivationPathSpec: DerivationPath.BIP44, + extendedPublicKeys: [{ type: 'extended-pubkey', key: '5678' }] as any, + publicKeys: [], }); expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { ...existingSecrets, @@ -445,9 +486,9 @@ describe('src/background/services/secrets/SecretsService.ts', () => { { id: uuid, secretType: SecretType.Ledger, - derivationPath: DerivationPath.BIP44, - xpub: 'xpub', - xpubXP: 'xpubXP', + derivationPathSpec: DerivationPath.BIP44, + extendedPublicKeys: [{ type: 'extended-pubkey', key: '5678' }], + publicKeys: [], name: 'Ledger 02', }, ], @@ -463,7 +504,7 @@ describe('src/background/services/secrets/SecretsService.ts', () => { eventListener, ); const existingSecrets = { - xpub: 'xpub', + extendedPublicKeys: [{ type: 'extended-pubkey', key: 'oldXpubXP' }], wallets: [ { id: ACTIVE_WALLET_ID, @@ -473,13 +514,22 @@ describe('src/background/services/secrets/SecretsService.ts', () => { storageService.load.mockResolvedValue(existingSecrets); await secretsService.updateSecrets( - { xpubXP: 'xpubXP' }, + { + extendedPublicKeys: [ + { type: 'extended-pubkey', key: 'xpubXP' }, + ] as any, + }, ACTIVE_WALLET_ID, ); expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { ...existingSecrets, - wallets: [{ ...existingSecrets.wallets[0], xpubXP: 'xpubXP' }], + wallets: [ + { + ...existingSecrets.wallets[0], + extendedPublicKeys: [{ type: 'extended-pubkey', key: 'xpubXP' }], + }, + ], }); expect(eventListener).toHaveBeenCalled(); @@ -783,18 +833,8 @@ describe('src/background/services/secrets/SecretsService.ts', () => { pkAcc, fbAcc, }); - const { mnemonic, xpub, xpubXP, derivationPath, id } = secrets.wallets[0]; expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { - wallets: [ - { - derivationPath, - mnemonic, - secretType: SecretType.Mnemonic, - xpub, - xpubXP, - id, - }, - ], + wallets: secrets.wallets, importedAccounts: { wcAcc, }, @@ -821,16 +861,7 @@ describe('src/background/services/secrets/SecretsService.ts', () => { ]); expect(result).toBe(0); expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { - wallets: [ - { - derivationPath: DerivationPath.BIP44, - id: 'active-wallet-id', - mnemonic: 'mnemonic', - secretType: SecretType.Mnemonic, - xpub: 'xpub', - xpubXP: 'xpubXP', - }, - ], + wallets: mockMnemonicWallet().wallets, }); }); }); @@ -936,11 +967,12 @@ describe('src/background/services/secrets/SecretsService.ts', () => { it('returns the policy details correctly for Ledger Live', async () => { mockLedgerLiveWallet({ - pubKeys: [ - null, + publicKeys: [ { + type: 'address-pubkey', + derivationPath: `m/44'/60'/1'/0/0`, btcWalletPolicyDetails, - } as PubKeyType, + }, ], }); @@ -959,7 +991,9 @@ describe('src/background/services/secrets/SecretsService.ts', () => { it('returns the policy details correctly for Ledger + BIP44', async () => { mockLedgerWallet({ - btcWalletPolicyDetails, + extendedPublicKeys: [ + { type: 'extended-pubkey', btcWalletPolicyDetails }, + ], }); await expect( @@ -998,22 +1032,15 @@ describe('src/background/services/secrets/SecretsService.ts', () => { mockLedgerLiveWallet(); expect( - await secretsService.isKnownSecret(SecretType.LedgerLive, [ - { evm: 'evm', xp: 'xp' }, - ]), + await secretsService.isKnownSecret(SecretType.LedgerLive, 'evm'), ).toBe(true); expect( - await secretsService.isKnownSecret(SecretType.LedgerLive, [ - { evm: 'evm', xp: 'xp' }, - { evm: 'new evm', xp: 'new xp' }, - ]), + await secretsService.isKnownSecret(SecretType.LedgerLive, 'xp'), ).toBe(true); expect( - await secretsService.isKnownSecret(SecretType.LedgerLive, [ - { evm: 'new evm', xp: 'new xp' }, - ]), + await secretsService.isKnownSecret(SecretType.LedgerLive, 'new evm'), ).toBe(false); }); }); @@ -1053,7 +1080,7 @@ describe('src/background/services/secrets/SecretsService.ts', () => { describe('Derivation path: Ledger Live', () => { it('throws if storage is empty for the given index', async () => { mockLedgerLiveWallet({ - pubKeys: [], + publicKeys: [], }); await expect(storeBtcWalletPolicyDetails()).rejects.toThrow( @@ -1063,8 +1090,11 @@ describe('src/background/services/secrets/SecretsService.ts', () => { it('throws if storage already contains policy info for the given index', async () => { mockLedgerLiveWallet({ - pubKeys: [ + publicKeys: [ { + type: 'extended-pubkey', + derivationPath: `m/44'/60'/${activeAccountData.index}'/0/0`, + key: 'key', btcWalletPolicyDetails: {}, }, ], @@ -1077,10 +1107,18 @@ describe('src/background/services/secrets/SecretsService.ts', () => { it('stores the policy details correctly', async () => { const existingSecrets = mockLedgerLiveWallet({ - pubKeys: [ + publicKeys: [ + { + key: '0x1', + type: 'address-pubkey', + derivationPath: `m/44'/60'/${activeAccountData.index}'/0/0`, + curve: 'secp256k1', + }, { - evm: '0x1', - xp: '0x2', + key: '0x2', + type: 'address-pubkey', + derivationPath: `m/44'/9000'/${activeAccountData.index}'/0/0`, + curve: 'secp256k1', }, ], }); @@ -1090,12 +1128,13 @@ describe('src/background/services/secrets/SecretsService.ts', () => { expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { wallets: [ { - derivationPath: existingSecrets.wallets[0]?.derivationPath, + derivationPathSpec: + existingSecrets.wallets[0]?.derivationPathSpec, id: ACTIVE_WALLET_ID, secretType: SecretType.LedgerLive, - pubKeys: [ + publicKeys: [ { - ...existingSecrets.wallets[0]?.pubKeys[0], + ...existingSecrets.wallets[0]?.publicKeys[0], btcWalletPolicyDetails: { xpub, masterFingerprint, @@ -1103,6 +1142,7 @@ describe('src/background/services/secrets/SecretsService.ts', () => { name, }, }, + existingSecrets.wallets[0]?.publicKeys[1], ], }, ], @@ -1113,7 +1153,12 @@ describe('src/background/services/secrets/SecretsService.ts', () => { describe('Derivation path: BIP44', () => { it('throws if storage is already contains policy', async () => { mockLedgerWallet({ - btcWalletPolicyDetails: {}, + extendedPublicKeys: [ + { + ...utils.buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + btcWalletPolicyDetails: {}, + }, + ], }); await expect(storeBtcWalletPolicyDetails()).rejects.toThrow( @@ -1122,24 +1167,31 @@ describe('src/background/services/secrets/SecretsService.ts', () => { }); it('stores the policy details correctly', async () => { - const existingSecrets = mockLedgerWallet(); + const existingSecrets = mockLedgerWallet({ + extendedPublicKeys: [ + { + ...utils.buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + }, + ], + }); await storeBtcWalletPolicyDetails(); expect(storageService.save).toHaveBeenCalledWith(WALLET_STORAGE_KEY, { wallets: [ { - id: ACTIVE_WALLET_ID, - derivationPath: existingSecrets.wallets[0]?.derivationPath, - btcWalletPolicyDetails: { - xpub, - masterFingerprint, - hmacHex, - name, - }, - xpub: existingSecrets.wallets[0]?.xpub, - xpubXP: existingSecrets.wallets[0]?.xpubXP, - secretType: SecretType.Ledger, + ...existingSecrets.wallets[0], + extendedPublicKeys: [ + { + ...existingSecrets.wallets[0]?.extendedPublicKeys[0], + btcWalletPolicyDetails: { + xpub, + masterFingerprint, + hmacHex, + name, + }, + }, + ], }, ], }); @@ -1149,61 +1201,43 @@ describe('src/background/services/secrets/SecretsService.ts', () => { describe('addAddress', () => { let ledgerService: LedgerService; - const addressesMock = { - addressC: 'addressC', - addressBTC: 'addressBTC', - addressAVM: 'addressAVM', - addressPVM: 'addressPVM', - addressCoreEth: 'addressCoreEth', - }; - let getAddressesSpy: jest.SpyInstance; + const addressResolver = { + getDerivationPaths: jest.fn(), + } as any; beforeEach(() => { - getAddressesSpy = jest.spyOn(secretsService as any, 'getAddresses'); - getAddressesSpy.mockReturnValue(addressesMock); ledgerService = new LedgerService(); - }); - - it('returns the result of getAddresses', async () => { - mockMnemonicWallet(); - - const result = await secretsService.addAddress({ - index: 1, - walletId: ACTIVE_WALLET_ID, - ledgerService, - networkService, - }); - expect(getAddressesSpy).toHaveBeenCalledWith( - 1, - ACTIVE_WALLET_ID, - networkService, - ); - expect(result).toStrictEqual(addressesMock); + addressResolver.getDerivationPaths.mockImplementation((accountIndex) => ({ + [NetworkVMType.EVM]: `m/44'/60'/0'/0/${accountIndex}`, + [NetworkVMType.AVM]: `m/44'/9000'/0'/0/${accountIndex}`, + [NetworkVMType.HVM]: `m/44'/9000'/0'/0'/${accountIndex}'`, + })); }); describe('ledger', () => { it('throws if transport is not available', async () => { mockLedgerLiveWallet({ - pubKeys: [], + publicKeys: [], }); jest .spyOn(ledgerService, 'recentTransport', 'get') .mockReturnValue(undefined); - await expect( + await expectToThrowErroCode( secretsService.addAddress({ index: 1, walletId: ACTIVE_WALLET_ID, ledgerService, - networkService, + addressResolver, }), - ).rejects.toThrow('Ledger transport not available'); + LedgerError.TransportNotFound, + ); }); it('throws when it fails to get EVM pubkey from ledger', async () => { const transportMock = {} as LedgerTransport; mockLedgerLiveWallet({ - pubKeys: [], + publicKeys: [], }); jest @@ -1214,25 +1248,27 @@ describe('src/background/services/secrets/SecretsService.ts', () => { Buffer.from(''), ); - await expect( + await expectToThrowErroCode( secretsService.addAddress({ index: 1, walletId: ACTIVE_WALLET_ID, ledgerService, - networkService, + addressResolver, }), - ).rejects.toThrow('Failed to get public key from device.'); + LedgerError.NoPublicKeyReturned, + ); expect(getPubKeyFromTransport).toHaveBeenCalledWith( transportMock, 1, DerivationPath.LedgerLive, + 'EVM', ); }); it('throws when it fails to get X/P pubkey from ledger', async () => { const transportMock = {} as LedgerTransport; mockLedgerLiveWallet({ - pubKeys: [], + publicKeys: [], }); jest .spyOn(ledgerService, 'recentTransport', 'get') @@ -1242,306 +1278,164 @@ describe('src/background/services/secrets/SecretsService.ts', () => { .mockReturnValueOnce(Buffer.from('evm')) .mockReturnValueOnce(Buffer.from('')); - await expect( + await expectToThrowErroCode( secretsService.addAddress({ index: 1, walletId: ACTIVE_WALLET_ID, ledgerService, - networkService, + addressResolver, }), - ).rejects.toThrow('Failed to get public key from device.'); + LedgerError.NoPublicKeyReturned, + ); + expect(getPubKeyFromTransport).toHaveBeenCalledWith( + transportMock, + 1, + DerivationPath.LedgerLive, + 'EVM', + ); expect(getPubKeyFromTransport).toHaveBeenCalledWith( transportMock, 1, DerivationPath.LedgerLive, + 'AVM', ); }); it('uses pubkey if index is already known', async () => { + jest.spyOn(utils, 'hasPublicKeyFor').mockReturnValue(true); + jest + .spyOn(ledgerService, 'recentTransport', 'get') + .mockReturnValue({} as LedgerTransport); + const addressBuffEvm = Buffer.from('0x1'); const addressBuffXP = Buffer.from('0x2'); - getAddressesSpy.mockReturnValueOnce(addressesMock); mockLedgerLiveWallet({ - pubKeys: [ + publicKeys: [ { - evm: addressBuffEvm.toString('hex'), - xp: addressBuffXP.toString('hex'), + type: 'address-pubkey', + key: addressBuffEvm.toString('hex'), + derivationPath: `m/44'/60'/0'/0/0`, + curve: 'secp256k1', + }, + { + type: 'address-pubkey', + key: addressBuffXP.toString('hex'), + derivationPath: `m/44'/9000'/0'/0/0`, + curve: 'secp256k1', }, ], }); - const result = await secretsService.addAddress({ + await secretsService.addAddress({ index: 0, walletId: ACTIVE_WALLET_ID, ledgerService, - networkService, + addressResolver, }); - expect(getAddressesSpy).toHaveBeenCalledWith( - 0, - ACTIVE_WALLET_ID, - networkService, - ); secretsService.updateSecrets = jest.fn(); expect(getPubKeyFromTransport).not.toHaveBeenCalled(); - expect(result).toStrictEqual(addressesMock); expect(secretsService.updateSecrets).not.toHaveBeenCalled(); }); - - it('gets the addresses correctly', async () => { - const addressBuffEvm = Buffer.from('0x1'); - const addressBuffXP = Buffer.from('0x2'); - const transportMock = {} as LedgerTransport; - secretsService.updateSecrets = jest.fn(); - getAddressesSpy.mockReturnValueOnce(addressesMock); - mockLedgerLiveWallet({ - pubKeys: [], - }); - jest - .spyOn(ledgerService, 'recentTransport', 'get') - .mockReturnValue(transportMock); - (getPubKeyFromTransport as jest.Mock) - .mockReturnValueOnce(addressBuffEvm) - .mockReturnValueOnce(addressBuffXP); - - const result = await secretsService.addAddress({ - index: 0, - walletId: ACTIVE_WALLET_ID, - ledgerService, - networkService, - }); - expect(getAddressesSpy).toHaveBeenCalledWith( - 0, - ACTIVE_WALLET_ID, - networkService, - ); - expect(result).toStrictEqual(addressesMock); - - expect(secretsService.updateSecrets).toHaveBeenCalledWith( - { - pubKeys: [ - { - evm: addressBuffEvm.toString('hex'), - xp: addressBuffXP.toString('hex'), - }, - ], - }, - ACTIVE_WALLET_ID, - ); - }); }); describe('seedless', () => { - const oldKeys = [{ evm: 'evm', xp: 'xp' }]; - const newKeys = [...oldKeys, { evm: 'evm2', xp: 'xp2' }]; + const oldKeys = [ + { + type: 'address-pubkey', + key: 'xp', + derivationPath: `m/44'/60'/0'/0/0`, + curve: 'secp256k1', + }, + { + type: 'address-pubkey', + key: 'evm', + derivationPath: `m/44'/9000'/0'/0/0`, + curve: 'secp256k1', + }, + ]; + const newKeys = [ + ...oldKeys, + { + type: 'address-pubkey', + key: 'xp2', + derivationPath: `m/44'/60'/0'/0/1`, + curve: 'secp256k1', + }, + { + type: 'address-pubkey', + key: 'evm2', + derivationPath: `m/44'/9000'/0'/0/1`, + curve: 'secp256k1', + }, + ]; const seedlessWalletMock = Object.create(SeedlessWallet.prototype); describe('when public keys for given account are not known yet', () => { beforeEach(() => { mockSeedlessWallet({ - pubKeys: oldKeys, + publicKeys: oldKeys, }); jest.mocked(SeedlessWallet).mockReturnValue(seedlessWalletMock); jest .spyOn(seedlessWalletMock, 'getPublicKeys') .mockResolvedValue(newKeys); - - jest - .spyOn(secretsService, 'getAddresses') - .mockResolvedValueOnce(addressesMock as any); }); it('calls addAccount on SeedlessWallet', async () => { secretsService.updateSecrets = jest.fn(); - const result = await secretsService.addAddress({ + await secretsService.addAddress({ index: 1, walletId: ACTIVE_WALLET_ID, - networkService, ledgerService, + addressResolver, }); expect(SeedlessWallet).toHaveBeenCalledWith({ - networkService, sessionStorage: expect.any(SeedlessTokenStorage), - addressPublicKey: { evm: 'evm', xp: 'xp' }, + addressPublicKey: oldKeys[0], }); expect(seedlessWalletMock.addAccount).toHaveBeenCalledWith(1); expect(secretsService.updateSecrets).toHaveBeenCalledWith( { - pubKeys: newKeys, + publicKeys: newKeys, }, ACTIVE_WALLET_ID, ); - expect(getAddressesSpy).toHaveBeenCalledWith( - 1, - ACTIVE_WALLET_ID, - networkService, - ); - expect(result).toStrictEqual(addressesMock); }); }); describe('when the public keys for the new account are known', () => { beforeEach(() => { mockSeedlessWallet({ - pubKeys: newKeys, + publicKeys: newKeys, }); + jest.spyOn(utils, 'hasPublicKeyFor').mockReturnValue(true); }); - it('retrieves the addresses without contacting seedless api', async () => { + it('does not update secrets', async () => { secretsService.updateSecrets = jest.fn(); - const addressBuffEvm = Buffer.from('0x1'); - const addressBuffXP = Buffer.from('0x2'); - - getAddressesSpy.mockReturnValueOnce(addressesMock); - - jest - .mocked(getPubKeyFromTransport) - .mockReturnValueOnce(addressBuffEvm as any) - .mockReturnValueOnce(addressBuffXP as any); - const result = await secretsService.addAddress({ + await secretsService.addAddress({ index: 1, walletId: ACTIVE_WALLET_ID, - networkService, ledgerService, + addressResolver, }); expect(SeedlessWallet).not.toHaveBeenCalled(); expect(secretsService.updateSecrets).not.toHaveBeenCalled(); - - expect(getAddressesSpy).toHaveBeenCalledWith( - 1, - ACTIVE_WALLET_ID, - networkService, - ); - expect(result).toStrictEqual(addressesMock); }); }); }); }); - describe('getAddresses', () => { - const addressesMock = (addressC: string, addressBTC: string) => ({ - [NetworkVMType.EVM]: addressC, - [NetworkVMType.BITCOIN]: addressBTC, - [NetworkVMType.AVM]: 'X-', - [NetworkVMType.PVM]: 'P-', - [NetworkVMType.CoreEth]: 'C-', - [NetworkVMType.HVM]: 'hvm-address', - }); - - it('throws error if walletId is not provided', async () => { - await expect( - secretsService.getAddresses(0, '', networkService), - ).rejects.toThrow('Wallet id not provided'); - }); - - it('throws if storage is empty', async () => { - mockMnemonicWallet( - {}, - { secretType: 'unknown', id: 'seedless-wallet-id' }, - ); - await expect( - secretsService.getAddresses(0, 'seedless-wallet-id', networkService), - ).rejects.toThrow('No public key available'); - }); - - it('should return the addresses for mnemonic', async () => { - mockMnemonicWallet(); - (getAddressFromXPub as jest.Mock).mockReturnValueOnce('0x1'); - (getAccountPrivateKeyFromMnemonic as jest.Mock).mockReturnValue( - 'privkey', - ); - (getAddressForHvm as jest.Mock).mockReturnValue('0xhvm'); - (getBech32AddressFromXPub as jest.Mock).mockReturnValueOnce('0x2'); - await expect( - secretsService.getAddresses(0, ACTIVE_WALLET_ID, networkService), - ).resolves.toStrictEqual({ - ...addressesMock('0x1', '0x2'), - HVM: '0xhvm', - }); - }); - it('returns the addresses for xpub', async () => { - mockLedgerWallet(); - (getAddressFromXPub as jest.Mock).mockReturnValueOnce('0x1'); - (getBech32AddressFromXPub as jest.Mock).mockReturnValueOnce('0x2'); - await expect( - secretsService.getAddresses(0, ACTIVE_WALLET_ID, networkService), - ).resolves.toStrictEqual({ - ...addressesMock('0x1', '0x2'), - HVM: undefined, - }); - expect(Avalanche.getAddressPublicKeyFromXpub).toBeCalledWith('xpubXP', 0); - expect(getAddressFromXPub).toHaveBeenCalledWith('xpub', 0); - expect(getBech32AddressFromXPub).toHaveBeenCalledWith( - 'xpub', - 0, - networks.testnet, - ); - }); - - it('throws if ledger pubkey is missing from storage', async () => { - mockLedgerLiveWallet({ - pubKeys: [], - }); - (networkService.isMainnet as jest.Mock).mockReturnValueOnce(false); - - await expect( - secretsService.getAddresses(0, ACTIVE_WALLET_ID, networkService), - ).rejects.toThrow('Account not added'); - }); - - it('returns the addresses for pubKey', async () => { - const pubKeyBuff = Buffer.from('pubKey', 'hex'); - mockLedgerLiveWallet({ - pubKeys: [{ evm: 'pubKey', xp: 'pubKeyXP' }], - }); - (networkService.isMainnet as jest.Mock).mockReturnValueOnce(false); - (getEvmAddressFromPubKey as jest.Mock).mockReturnValueOnce('0x1'); - (getBtcAddressFromPubKey as jest.Mock).mockReturnValueOnce('0x2'); - - await expect( - secretsService.getAddresses(0, ACTIVE_WALLET_ID, networkService), - ).resolves.toStrictEqual({ - ...addressesMock('0x1', '0x2'), - HVM: undefined, - }); - - expect(getEvmAddressFromPubKey).toHaveBeenCalledWith(pubKeyBuff); - expect(getBtcAddressFromPubKey).toHaveBeenCalledWith( - pubKeyBuff, - networks.testnet, - ); - expect(getAddressMock).toHaveBeenNthCalledWith( - 1, - expect.any(Buffer), - 'X', - ); - expect(getAddressMock).toHaveBeenNthCalledWith( - 2, - expect.any(Buffer), - 'P', - ); - expect(getAddressMock).toHaveBeenNthCalledWith( - 3, - expect.any(Buffer), - 'C', - ); - }); - }); - describe('addImportedWallet', () => { - const pubKeyBuffer = Buffer.from('0x111', 'hex'); - - beforeEach(() => { - (networkService.isMainnet as jest.Mock).mockReturnValue(false); - (getPublicKeyFromPrivateKey as jest.Mock).mockReturnValue(pubKeyBuffer); - (getEvmAddressFromPubKey as jest.Mock).mockReturnValue('0x1'); - (getBtcAddressFromPubKey as jest.Mock).mockReturnValue('0x2'); - }); + const addressResolver = { + getAddressesForSecretId: jest.fn(), + } as unknown as AddressResolver; it('saves the secret in storage', async () => { secretsService.saveImportedWallet = jest.fn(); @@ -1552,23 +1446,22 @@ describe('src/background/services/secrets/SecretsService.ts', () => { imported: {}, }); + jest + .mocked(addressResolver.getAddressesForSecretId) + .mockResolvedValue(utils.emptyAddresses()); + const result = await secretsService.addImportedWallet( { importType: ImportType.PRIVATE_KEY, data: 'privateKey', }, - networkService, + addressResolver, ); expect(result).toStrictEqual({ account: { id: uuid, - addressBTC: '0x2', - addressC: '0x1', - addressAVM: 'X-', - addressPVM: 'P-', - addressCoreEth: 'C-', - addressHVM: hvmAddress, + ...mapVMAddresses(utils.emptyAddresses()), }, commit: expect.any(Function), }); @@ -1582,50 +1475,10 @@ describe('src/background/services/secrets/SecretsService.ts', () => { }); }); - it('throws if unable to calculate public key', async () => { - (getPublicKeyFromPrivateKey as jest.Mock).mockImplementationOnce(() => { - throw new Error('foo'); - }); - - mockMnemonicWallet({ - imported: {}, - }); - - await expect( - secretsService.addImportedWallet( - { - importType: ImportType.PRIVATE_KEY, - data: 'privateKey', - }, - networkService, - ), - ).rejects.toThrow('Error while calculating addresses'); - }); - - it('throws if unable to calculate EVM address', async () => { - (getEvmAddressFromPubKey as jest.Mock).mockImplementationOnce(() => { - throw new Error('foo'); - }); - - mockMnemonicWallet({ - imported: {}, - }); - - await expect( - secretsService.addImportedWallet( - { - importType: ImportType.PRIVATE_KEY, - data: 'privateKey', - }, - networkService, - ), - ).rejects.toThrow('Error while calculating addresses'); - }); - - it('throws if unable to calculate BTC address', async () => { - (getBtcAddressFromPubKey as jest.Mock).mockImplementationOnce(() => { - throw new Error('foo'); - }); + it('throws if unable to resolve addresses', async () => { + jest + .mocked(addressResolver.getAddressesForSecretId) + .mockRejectedValueOnce(new Error('Error while calculating addresses')); mockMnemonicWallet({ imported: {}, @@ -1637,103 +1490,12 @@ describe('src/background/services/secrets/SecretsService.ts', () => { importType: ImportType.PRIVATE_KEY, data: 'privateKey', }, - networkService, + addressResolver, ), ).rejects.toThrow('Error while calculating addresses'); }); }); - describe('getImportedAddresses', () => { - const pubKeyBuffer = Buffer.from('0x111', 'hex'); - - beforeEach(() => { - (networkService.isMainnet as jest.Mock).mockReturnValue(true); - (getPublicKeyFromPrivateKey as jest.Mock).mockReturnValue(pubKeyBuffer); - (getEvmAddressFromPubKey as jest.Mock).mockReturnValue('0x1'); - (getBtcAddressFromPubKey as jest.Mock).mockReturnValue('0x2'); - }); - - it('throws if imported account is missing from storage', async () => { - secretsService.getImportedAccountSecrets = jest.fn(); - (secretsService.getImportedAccountSecrets as jest.Mock).mockRejectedValue( - new Error('No secrets found for imported account'), - ); - - await expect( - secretsService.getImportedAddresses('id', networkService), - ).rejects.toThrow('No secrets found for imported account'); - }); - - it('throws if importType is not supported', async () => { - secretsService.getImportedAccountSecrets = jest.fn(); - (secretsService.getImportedAccountSecrets as jest.Mock).mockResolvedValue( - { secretType: 'unknown' as any, secret: 'secret' }, - ); - - await expect( - secretsService.getImportedAddresses('id', networkService), - ).rejects.toThrow('Unsupported import type'); - }); - - it('throws if addresses are missing', async () => { - (networkService.isMainnet as jest.Mock).mockReturnValue(false); - secretsService.getImportedAccountSecrets = jest.fn(); - (secretsService.getImportedAccountSecrets as jest.Mock).mockResolvedValue( - { secretType: SecretType.PrivateKey, secret: 'secret' }, - ); - (getEvmAddressFromPubKey as jest.Mock).mockReturnValueOnce(''); - (getBtcAddressFromPubKey as jest.Mock).mockReturnValueOnce(''); - - await expect( - secretsService.getImportedAddresses('id', networkService), - ).rejects.toThrow('Missing address'); - }); - - it('returns the addresses for PRIVATE_KEY correctly', async () => { - (networkService.isMainnet as jest.Mock).mockReturnValue(false); - secretsService.getImportedAccountSecrets = jest.fn(); - (secretsService.getImportedAccountSecrets as jest.Mock).mockResolvedValue( - { secretType: SecretType.PrivateKey, secret: 'secret' }, - ); - - const result = await secretsService.getImportedAddresses( - 'id', - networkService, - ); - - expect(result).toStrictEqual({ - addressBTC: '0x2', - addressC: '0x1', - addressAVM: 'X-', - addressPVM: 'P-', - addressCoreEth: 'C-', - addressHVM: undefined, - }); - - expect(getPublicKeyFromPrivateKey).toHaveBeenCalledWith('secret'); - expect(getEvmAddressFromPubKey).toHaveBeenCalledWith(pubKeyBuffer); - expect(getBtcAddressFromPubKey).toHaveBeenCalledWith( - pubKeyBuffer, - networks.testnet, - ); - expect(getAddressMock).toHaveBeenNthCalledWith( - 1, - expect.any(Buffer), - 'X', - ); - expect(getAddressMock).toHaveBeenNthCalledWith( - 2, - expect.any(Buffer), - 'P', - ); - expect(getAddressMock).toHaveBeenNthCalledWith( - 3, - expect.any(Buffer), - 'C', - ); - }); - }); - describe('deleteImportedWallets', () => { it('deletes the provided ids from storage', async () => { mockMnemonicWallet({ diff --git a/src/background/services/secrets/SecretsService.ts b/src/background/services/secrets/SecretsService.ts index b58187c34..9bf3f3dda 100644 --- a/src/background/services/secrets/SecretsService.ts +++ b/src/background/services/secrets/SecretsService.ts @@ -1,6 +1,7 @@ import { omit, pick } from 'lodash'; import { singleton } from 'tsyringe'; +import EventEmitter from 'events'; import { Account, AccountType, @@ -14,38 +15,33 @@ import { WALLET_STORAGE_KEY, AddPrimaryWalletSecrets, WalletDetails, - PubKeyType, WalletEvents, } from '../wallet/models'; import { - DerivedAddresses, + AddressPublicKeyJson, + Curve, + EVM_BASE_DERIVATION_PATH, ImportedAccountSecrets, PrimaryWalletSecrets, SecretType, } from './models'; import { isPrimaryAccount } from '../accounts/utils/typeGuards'; -import _ from 'lodash'; import { - Avalanche, - getAddressFromXPub, - getAddressPublicKeyFromXPub, - getBech32AddressFromXPub, - getBtcAddressFromPubKey, - getEvmAddressFromPubKey, getPubKeyFromTransport, - getPublicKeyFromPrivateKey, + DerivationPath, } from '@avalabs/core-wallets-sdk'; -import { networks } from 'bitcoinjs-lib'; -import { NetworkVMType } from '@avalabs/core-chains-sdk'; +import { NetworkVMType } from '@avalabs/vm-module-types'; import { SeedlessWallet } from '../seedless/SeedlessWallet'; import { SeedlessTokenStorage } from '../seedless/SeedlessTokenStorage'; import { LedgerService } from '../ledger/LedgerService'; -import { NetworkService } from '../network/NetworkService'; import { WalletConnectService } from '../walletConnect/WalletConnectService'; -import EventEmitter from 'events'; import { OnUnlock } from '@src/background/runtime/lifecycleCallbacks'; -import { getAddressForHvm } from './utils/getAddressForHvm'; -import { getAccountPrivateKeyFromMnemonic } from './utils/getAccountPrivateKeyFromMnemonic'; +import { hasPublicKeyFor } from './utils'; +import { AddressPublicKey } from './AddressPublicKey'; +import { AddressResolver } from './AddressResolver'; +import { mapVMAddresses } from '../accounts/utils/mapVMAddresses'; +import { assertPresent } from '@src/utils/assertions'; +import { LedgerError } from '@src/utils/errors'; /** * Use this service to fetch, save or delete account secrets. @@ -54,6 +50,13 @@ import { getAccountPrivateKeyFromMnemonic } from './utils/getAccountPrivateKeyFr export class SecretsService implements OnUnlock { #eventEmitter = new EventEmitter(); + /** + * This is used when we need to store a secret temporarily, + * like when we need to calculate addresses for a private key + * before letting the AccountService commit the secret. + */ + #temporarySecretStorage = new Map(); + constructor(private storageService: StorageService) {} async #getDefaultName(secrets: AddPrimaryWalletSecrets) { @@ -124,7 +127,7 @@ export class SecretsService implements OnUnlock { id: wallet.id, name: wallet.name, type: wallet.secretType, - derivationPath: wallet.derivationPath, + derivationPath: wallet.derivationPathSpec as DerivationPath, authProvider: wallet.authProvider, userEmail: wallet.userEmail, userId: wallet.userId, @@ -135,7 +138,7 @@ export class SecretsService implements OnUnlock { id: wallet.id, name: wallet.name, type: wallet.secretType, - derivationPath: wallet.derivationPath, + derivationPath: wallet.derivationPathSpec as DerivationPath, }; }); } @@ -291,6 +294,32 @@ export class SecretsService implements OnUnlock { }; } + async getSecretsById(walletId: string) { + const tempSecret = this.#temporarySecretStorage.get(walletId); + + if (tempSecret) { + return tempSecret; + } + + const secrets = await this.#loadSecrets(true); + + const importedAccountSecrets = secrets.importedAccounts?.[walletId]; + + if (importedAccountSecrets) { + return importedAccountSecrets; + } + + const primaryWalletSecrets = secrets.wallets.find( + ({ id }) => id === walletId, + ); + + if (!primaryWalletSecrets) { + throw new Error('No secrets found for this id'); + } + + return primaryWalletSecrets; + } + async getWalletAccountsSecretsById(walletId: string) { const walletKeys = await this.#loadSecrets(true); @@ -383,8 +412,9 @@ export class SecretsService implements OnUnlock { const { account } = secrets; if (secrets.secretType === SecretType.LedgerLive && account) { - const pubKeys = secrets.pubKeys ?? []; - const pubKeyInfo = pubKeys[account.index]; + const pubKeyInfo = secrets.publicKeys.find( + (key) => key.derivationPath === `m/44'/60'/${account.index}'/0/0`, + ); if (!pubKeyInfo) { throw new Error( @@ -405,31 +435,40 @@ export class SecretsService implements OnUnlock { name, }; - pubKeys[account.index] = pubKeyInfo; + secrets.publicKeys[account.index] = pubKeyInfo; return await this.updateSecrets( { - pubKeys, + publicKeys: secrets.publicKeys, }, walletId, ); } if (secrets.secretType === SecretType.Ledger) { - if (secrets?.btcWalletPolicyDetails) { + const extPubKey = secrets.extendedPublicKeys.find( + (key) => key.derivationPath === EVM_BASE_DERIVATION_PATH, + ); + if (!extPubKey) { + throw new Error('No matching extended public key found'); + } + + if (extPubKey?.btcWalletPolicyDetails) { throw new Error( 'Error while saving wallet policy details: policy details already stored.', ); } + extPubKey.btcWalletPolicyDetails = { + xpub, + masterFingerprint, + hmacHex, + name, + }; + return await this.updateSecrets( { - btcWalletPolicyDetails: { - xpub, - masterFingerprint, - hmacHex, - name, - }, + extendedPublicKeys: secrets.extendedPublicKeys, }, walletId, ); @@ -452,7 +491,9 @@ export class SecretsService implements OnUnlock { if (secrets.secretType === SecretType.LedgerLive && secrets.account) { const accountIndex = secrets.account.index; - const pubKeyInfo = secrets.pubKeys[accountIndex]; + const pubKeyInfo = secrets.publicKeys.find( + (key) => key.derivationPath === `m/44'/60'/${accountIndex}'/0/0`, // TODO: extract as util function + ); return { accountIndex, @@ -461,9 +502,13 @@ export class SecretsService implements OnUnlock { } if (secrets.secretType === SecretType.Ledger) { + const extPubKey = secrets.extendedPublicKeys.find( + (key) => key.btcWalletPolicyDetails, + ); + return { accountIndex: 0, - details: secrets.btcWalletPolicyDetails, + details: extPubKey?.btcWalletPolicyDetails, }; } } @@ -494,8 +539,12 @@ export class SecretsService implements OnUnlock { privateKey: string, ): Promise; async isKnownSecret( - type: SecretType.LedgerLive | SecretType.Ledger, - pub: string | PubKeyType[], + type: SecretType.LedgerLive, + publicKey: string, + ): Promise; + async isKnownSecret( + type: SecretType.Ledger, + extendedPublicKey: string, ): Promise; async isKnownSecret( type: @@ -533,7 +582,8 @@ export class SecretsService implements OnUnlock { if (type === SecretType.Ledger) { return secrets.wallets.some( (wallet) => - wallet.secretType === SecretType.Ledger && wallet.xpub === secret, + wallet.secretType === SecretType.Ledger && + wallet.extendedPublicKeys.some((pub) => pub.key === secret), ); } @@ -541,8 +591,7 @@ export class SecretsService implements OnUnlock { return secrets.wallets.some((wallet) => { return ( wallet.secretType === SecretType.LedgerLive && - Array.isArray(secret) && - _.isEqual(wallet.pubKeys[0], secret[0]) + wallet.publicKeys.some((pub) => pub.key === secret) ); }); } @@ -557,7 +606,7 @@ export class SecretsService implements OnUnlock { async addImportedWallet( importData: ImportData, - networkService: NetworkService, + addressResolver: AddressResolver, ) { const id = crypto.randomUUID(); @@ -600,265 +649,216 @@ export class SecretsService implements OnUnlock { } if (importData.importType === ImportType.PRIVATE_KEY) { - const addresses = await this.#calculateAddressesForPrivateKey( - importData.data, - networkService, - ); + this.#temporarySecretStorage.set(id, { + secretType: SecretType.PrivateKey, + secret: importData.data, + }); - return { - account: { - id, - ...addresses, - }, - commit, - }; + try { + const addresses = await addressResolver.getAddressesForSecretId(id); + return { + account: { + id, + ...mapVMAddresses(addresses), + }, + commit, + }; + } finally { + this.#temporarySecretStorage.delete(id); + } } throw new Error('Unknown import type'); } - async #calculateAddressesForPrivateKey( - privateKey: string, - networkService: NetworkService, - ): Promise { - const addresses: DerivedAddresses = { - addressBTC: '', - addressC: '', - addressAVM: '', - addressPVM: '', - addressCoreEth: '', - }; - - const provXP = await networkService.getAvalanceProviderXP(); - - try { - const publicKey = getPublicKeyFromPrivateKey(privateKey); - addresses.addressC = getEvmAddressFromPubKey(publicKey); - addresses.addressBTC = getBtcAddressFromPubKey( - publicKey, - networkService.isMainnet() ? networks.bitcoin : networks.testnet, - ); - addresses.addressAVM = provXP.getAddress(publicKey, 'X'); - addresses.addressPVM = provXP.getAddress(publicKey, 'P'); - addresses.addressCoreEth = provXP.getAddress(publicKey, 'C'); - addresses.addressHVM = getAddressForHvm(privateKey); - } catch (_err) { - throw new Error('Error while calculating addresses'); - } - - if ( - !addresses.addressC || - !addresses.addressBTC || - !addresses.addressAVM || - !addresses.addressPVM || - !addresses.addressCoreEth - ) { - throw new Error(`Missing address`); - } - - return addresses; - } - async addAddress({ index, walletId, ledgerService, - networkService, + addressResolver, }: { index: number; walletId: string; ledgerService: LedgerService; - networkService: NetworkService; - }): Promise> { + addressResolver: AddressResolver; + }): Promise { const secrets = await this.getWalletAccountsSecretsById(walletId); + const derivationPaths = await addressResolver.getDerivationPaths( + index, + secrets.derivationPathSpec, + ); + const derivationPathEVM = derivationPaths[NetworkVMType.EVM]; + const derivationPathAVM = derivationPaths[NetworkVMType.AVM]; + const derivationPathHVM = derivationPaths[NetworkVMType.HVM]; + + const hasEVMPublicKey = hasPublicKeyFor( + secrets, + derivationPathEVM, + 'secp256k1', + ); + const hasAVMPublicKey = hasPublicKeyFor( + secrets, + derivationPathAVM, + 'secp256k1', + ); + if ( - secrets.secretType === SecretType.LedgerLive && - !secrets.pubKeys[index] + secrets.secretType === SecretType.Seedless && + (!hasAVMPublicKey || !hasEVMPublicKey) ) { - // With LedgerLive, we don't have xPub or Mnemonic, so we need - // to get the new address pubkey from the Ledger device. - if (!ledgerService.recentTransport) { - throw new Error('Ledger transport not available'); - } - - // Get EVM public key from transport - const addressPublicKeyC = await getPubKeyFromTransport( - ledgerService.recentTransport, - index, - secrets.derivationPath, - ); - - // Get X/P public key from transport - const addressPublicKeyXP = await getPubKeyFromTransport( - ledgerService.recentTransport, - index, - secrets.derivationPath, - 'AVM', - ); - - if ( - !addressPublicKeyC || - !addressPublicKeyC.byteLength || - !addressPublicKeyXP || - !addressPublicKeyXP.byteLength - ) { - throw new Error('Failed to get public key from device.'); - } - - const pubKeys = [...(secrets?.pubKeys || [])]; - pubKeys[index] = { - evm: addressPublicKeyC.toString('hex'), - xp: addressPublicKeyXP.toString('hex'), - }; - - await this.updateSecrets( - { - pubKeys, - }, - walletId, - ); - } - - if (secrets.secretType === SecretType.Seedless && !secrets.pubKeys[index]) { const wallet = new SeedlessWallet({ - networkService, sessionStorage: new SeedlessTokenStorage(this), - addressPublicKey: secrets.pubKeys[0], + addressPublicKey: secrets.publicKeys[0], }); // Prompt Core Seedless API to derive new keys await wallet.addAccount(index); - // Update the public keys in wallet + + // With Seedless, we're always getting the entire collection + // of derive public keys, so we replace the existing ones + // instead of appending new ones. await this.updateSecrets( { - pubKeys: await wallet.getPublicKeys(), + publicKeys: await wallet.getPublicKeys(), }, walletId, ); - } - const addresses = this.getAddresses(index, walletId, networkService); - return addresses; - } - - async getAddresses( - index: number, - walletId: string, - networkService: NetworkService, - ): Promise | never> { - if (!walletId) { - throw new Error('Wallet id not provided'); - } - const secrets = await this.getWalletAccountsSecretsById(walletId); - - if (!secrets) { - throw new Error('Wallet is not initialized'); + return; } - const isMainnet = networkService.isMainnet(); - const providerXP = await networkService.getAvalanceProviderXP(); + const newPublicKeys: AddressPublicKeyJson[] = []; - if ( - secrets.secretType === SecretType.Ledger || - secrets.secretType === SecretType.Mnemonic || - secrets.secretType === SecretType.Keystone - ) { - // C-avax... this address uses the same public key as EVM - const cPubkey = getAddressPublicKeyFromXPub(secrets.xpub, index); - const cAddr = providerXP.getAddress(cPubkey, 'C'); - - let xAddr: string | undefined = undefined; - let pAddr: string | undefined = undefined; - // We can only get X/P addresses if xpubXP is set - if (secrets.xpubXP) { - // X and P addresses different derivation path m/44'/9000'/0'... - const xpPub = Avalanche.getAddressPublicKeyFromXpub( - secrets.xpubXP, + if (secrets.secretType === SecretType.LedgerLive) { + if (!hasEVMPublicKey) { + assertPresent( + ledgerService.recentTransport, + LedgerError.TransportNotFound, + ); + const addressPublicKeyC = await getPubKeyFromTransport( + ledgerService.recentTransport, index, + secrets.derivationPathSpec as DerivationPath, + 'EVM', + ); + assertPresent( + addressPublicKeyC, + LedgerError.NoPublicKeyReturned, + `EVM @ ${derivationPathEVM}`, ); - xAddr = providerXP.getAddress(xpPub, 'X'); - pAddr = providerXP.getAddress(xpPub, 'P'); + newPublicKeys.push({ + curve: 'secp256k1', + derivationPath: derivationPathEVM, + key: addressPublicKeyC.toString('hex'), + type: 'address-pubkey', + }); } - return { - [NetworkVMType.EVM]: getAddressFromXPub(secrets.xpub, index), - [NetworkVMType.BITCOIN]: getBech32AddressFromXPub( - secrets.xpub, + if (!hasAVMPublicKey) { + assertPresent( + ledgerService.recentTransport, + LedgerError.TransportNotFound, + ); + const addressPublicKeyXP = await getPubKeyFromTransport( + ledgerService.recentTransport, index, - isMainnet ? networks.bitcoin : networks.testnet, - ), - [NetworkVMType.AVM]: xAddr, - [NetworkVMType.PVM]: pAddr, - [NetworkVMType.CoreEth]: cAddr, - [NetworkVMType.HVM]: - secrets.secretType === SecretType.Mnemonic - ? getAddressForHvm( - getAccountPrivateKeyFromMnemonic( - secrets.mnemonic, - index, - secrets.derivationPath, - ), - ) - : undefined, - }; - } - - if ( - secrets.secretType === SecretType.LedgerLive || - secrets.secretType === SecretType.Seedless - ) { - // pubkeys are used for LedgerLive derivation paths m/44'/60'/n'/0/0 - // and for X/P derivation paths m/44'/9000'/n'/0/0 - const addressPublicKey = secrets.pubKeys[index]; - - if (!addressPublicKey?.evm) { - throw new Error('Account not added'); + secrets.derivationPathSpec as DerivationPath, + 'AVM', + ); + assertPresent( + addressPublicKeyXP, + LedgerError.NoPublicKeyReturned, + `AVM @ ${derivationPathAVM}`, + ); + newPublicKeys.push({ + curve: 'secp256k1', + derivationPath: derivationPathAVM, + key: addressPublicKeyXP.toString('hex'), + type: 'address-pubkey', + }); } - - const pubKeyBuffer = Buffer.from(addressPublicKey.evm, 'hex'); - - // X/P addresses use a different public key because derivation path is different - let addrX, addrP; - if (addressPublicKey.xp) { - const pubKeyBufferXP = Buffer.from(addressPublicKey.xp, 'hex'); - addrX = providerXP.getAddress(pubKeyBufferXP, 'X'); - addrP = providerXP.getAddress(pubKeyBufferXP, 'P'); + } else if (secrets.secretType === SecretType.Ledger) { + // For Ledger, we can only use the extended public keys to + // derive EVM/Bitcoin & AVM public keys. + if (!hasEVMPublicKey) { + const publicKeyEVM = AddressPublicKey.fromExtendedPublicKeys( + secrets.extendedPublicKeys, + 'secp256k1', + derivationPathEVM, + ).toJSON(); + newPublicKeys.push(publicKeyEVM); } - return { - [NetworkVMType.EVM]: getEvmAddressFromPubKey(pubKeyBuffer), - [NetworkVMType.BITCOIN]: getBtcAddressFromPubKey( - pubKeyBuffer, - isMainnet ? networks.bitcoin : networks.testnet, - ), - [NetworkVMType.AVM]: addrX, - [NetworkVMType.PVM]: addrP, - [NetworkVMType.CoreEth]: providerXP.getAddress(pubKeyBuffer, 'C'), - [NetworkVMType.HVM]: undefined, - }; - } - - throw new Error('No public key available'); - } - - async getImportedAddresses(id: string, networkService: NetworkService) { - const secrets = await this.getImportedAccountSecrets(id); - - if ( - secrets.secretType === SecretType.WalletConnect || - secrets.secretType === SecretType.Fireblocks - ) { - return secrets.addresses; + if (!hasAVMPublicKey) { + const publicKeyAVM = AddressPublicKey.fromExtendedPublicKeys( + secrets.extendedPublicKeys, + 'secp256k1', + derivationPathAVM, + ).toJSON(); + newPublicKeys.push(publicKeyAVM); + } + } else if (secrets.secretType === SecretType.Mnemonic) { + // For mnemonic, we can derive public keys for EVM/Bitcoin, AVM and HVM + if (!hasEVMPublicKey) { + const publicKeyEVM = await AddressPublicKey.fromSeedphrase( + secrets.mnemonic, + 'secp256k1', + derivationPathEVM, + ); + newPublicKeys.push(publicKeyEVM.toJSON()); + } + if (!hasAVMPublicKey) { + const publicKeyAVM = await AddressPublicKey.fromSeedphrase( + secrets.mnemonic, + 'secp256k1', + derivationPathAVM, + ); + newPublicKeys.push(publicKeyAVM.toJSON()); + } + if (!hasPublicKeyFor(secrets, derivationPathHVM, 'ed25519')) { + const publicKeyHVM = await AddressPublicKey.fromSeedphrase( + secrets.mnemonic, + 'ed25519', + derivationPathHVM, + ); + newPublicKeys.push(publicKeyHVM.toJSON()); + } + } else if (secrets.secretType === SecretType.Keystone) { + // For Keystone, we can only derive EVM/Bitcoin public key. + if (!hasEVMPublicKey) { + const publicKeyEVM = AddressPublicKey.fromExtendedPublicKeys( + secrets.extendedPublicKeys, + 'secp256k1', + derivationPathEVM, + ).toJSON(); + newPublicKeys.push(publicKeyEVM); + } } - if (secrets.secretType === SecretType.PrivateKey) { - return this.#calculateAddressesForPrivateKey( - secrets.secret, - networkService, + if (newPublicKeys.length > 0) { + await this.updateSecrets( + { + publicKeys: [...secrets.publicKeys, ...newPublicKeys], + }, + walletId, ); } + } - throw new Error('Unsupported import type'); + async derivePublicKey( + secretId: string, + curve: Curve, + derivationPath?: string, + ): Promise { + const secrets = await this.getSecretsById(secretId); + + const pubkey = await AddressPublicKey.fromSecrets( + secrets, + curve, + derivationPath, + ); + + return pubkey.key; } } diff --git a/src/background/services/secrets/models.ts b/src/background/services/secrets/models.ts index 4e50e15e9..d51f6c946 100644 --- a/src/background/services/secrets/models.ts +++ b/src/background/services/secrets/models.ts @@ -1,4 +1,3 @@ -import { DerivationPath } from '@avalabs/core-wallets-sdk'; import { SignerSessionData } from '@cubist-labs/cubesigner-sdk'; import { @@ -12,6 +11,8 @@ import { PubKeyType, SeedlessAuthProvider, } from '../wallet/models'; +import { DerivationPath } from '@avalabs/core-wallets-sdk'; +import { NetworkVMType } from '@avalabs/vm-module-types'; export enum SecretType { // Primary wallet types @@ -26,6 +27,28 @@ export enum SecretType { Fireblocks = 'fireblocks', } +export type Secp256k1 = 'secp256k1'; +export type Ed25519 = 'ed25519'; +export type Curve = Secp256k1 | Ed25519; +export const EVM_BASE_DERIVATION_PATH = "m/44'/60'/0'"; +export const AVALANCHE_BASE_DERIVATION_PATH = "m/44'/9000'/0'"; + +export type AddressPublicKeyJson = { + type: 'address-pubkey'; + curve: Curve; + derivationPath: HasDerivationPath extends true ? string : null; + key: string; + btcWalletPolicyDetails?: BtcWalletPolicyDetails; +}; + +export type ExtendedPublicKey = { + type: 'extended-pubkey'; + curve: Secp256k1; + derivationPath: string; + key: string; + btcWalletPolicyDetails?: BtcWalletPolicyDetails; +}; + interface SecretsBase { secretType: SecretType; } @@ -35,48 +58,43 @@ interface PrimarySecretsBase extends SecretsBase { name: string; } -interface SeedlessSecrets extends PrimarySecretsBase { +export interface SeedlessSecrets extends PrimarySecretsBase { secretType: SecretType.Seedless; - pubKeys: PubKeyType[]; + publicKeys: AddressPublicKeyJson[]; + derivationPathSpec: DerivationPath.BIP44; seedlessSignerToken: SignerSessionData; - derivationPath: DerivationPath; authProvider: SeedlessAuthProvider; userEmail?: string; userId?: string; - mnemonic?: never; - xpub?: never; - xpubXP?: never; } -interface MnemonicSecrets extends PrimarySecretsBase { +export interface MnemonicSecrets extends PrimarySecretsBase { secretType: SecretType.Mnemonic; mnemonic: string; - xpub: string; - xpubXP: string; - derivationPath: DerivationPath; + extendedPublicKeys: ExtendedPublicKey[]; + publicKeys: AddressPublicKeyJson[]; + derivationPathSpec: DerivationPath.BIP44; } -interface KeystoneSecrets extends PrimarySecretsBase { +export interface KeystoneSecrets extends PrimarySecretsBase { secretType: SecretType.Keystone; masterFingerprint: string; - xpub: string; - xpubXP?: never; - derivationPath: DerivationPath; + publicKeys: AddressPublicKeyJson[]; + extendedPublicKeys: ExtendedPublicKey[]; + derivationPathSpec: DerivationPath.BIP44; } -interface LedgerSecrets extends PrimarySecretsBase { +export interface LedgerSecrets extends PrimarySecretsBase { secretType: SecretType.Ledger; - xpub: string; - xpubXP?: string; - derivationPath: DerivationPath.BIP44; - btcWalletPolicyDetails?: BtcWalletPolicyDetails; + publicKeys: AddressPublicKeyJson[]; + extendedPublicKeys: ExtendedPublicKey[]; + derivationPathSpec: DerivationPath.BIP44; } -interface LedgerLiveSecrets extends PrimarySecretsBase { +export interface LedgerLiveSecrets extends PrimarySecretsBase { secretType: SecretType.LedgerLive; - pubKeys: PubKeyType[]; - xpubXP?: never; - derivationPath: DerivationPath.LedgerLive; + publicKeys: AddressPublicKeyJson[]; + derivationPathSpec: DerivationPath.LedgerLive; } interface ImportedPrivateKeySecrets extends SecretsBase { @@ -125,3 +143,8 @@ export type DerivedAddresses = { addressCoreEth?: string; addressHVM?: string; }; + +export type DerivationPathsMap = Record< + Exclude, + string +>; diff --git a/src/background/services/secrets/utils.ts b/src/background/services/secrets/utils.ts new file mode 100644 index 000000000..86ab2549f --- /dev/null +++ b/src/background/services/secrets/utils.ts @@ -0,0 +1,116 @@ +import { NetworkVMType } from '@avalabs/vm-module-types'; + +import { + AddressPublicKeyJson, + Curve, + DerivationPathsMap, + ExtendedPublicKey, + ImportedAccountSecrets, + PrimaryWalletSecrets, + SecretType, +} from './models'; + +export const emptyAddresses = (): Record => ({ + [NetworkVMType.AVM]: '', + [NetworkVMType.BITCOIN]: '', + [NetworkVMType.CoreEth]: '', + [NetworkVMType.EVM]: '', + [NetworkVMType.HVM]: '', + [NetworkVMType.PVM]: '', + [NetworkVMType.SVM]: '', +}); + +export const emptyDerivationPaths = (): DerivationPathsMap => ({ + [NetworkVMType.AVM]: '', + [NetworkVMType.BITCOIN]: '', + [NetworkVMType.EVM]: '', + [NetworkVMType.HVM]: '', + [NetworkVMType.SVM]: '', +}); + +export const isImportedAccountSecrets = ( + secrets: PrimaryWalletSecrets | ImportedAccountSecrets, +): secrets is ImportedAccountSecrets => { + return [ + SecretType.PrivateKey, + SecretType.WalletConnect, + SecretType.Fireblocks, + ].includes(secrets.secretType); +}; + +export const isPrimaryWalletSecrets = ( + secrets: PrimaryWalletSecrets | ImportedAccountSecrets, +): secrets is PrimaryWalletSecrets => { + return !isImportedAccountSecrets(secrets); +}; + +export function assertDerivationPath(path?: string): asserts path is string { + if (typeof path !== 'string') { + throw new Error('Derivation path is required for primary wallets'); + } +} + +export function buildExtendedPublicKey( + key: string, + derivationPath: string, +): ExtendedPublicKey { + return { + type: 'extended-pubkey', + curve: 'secp256k1', + derivationPath, + key, + }; +} + +const findPublicKey = + (path: string, curve: Curve) => (pk: AddressPublicKeyJson) => + pk.derivationPath === path && pk.curve === curve; + +const findExtendedPublicKey = + (path: string, curve: Curve, exact = false) => + (extPk: ExtendedPublicKey) => { + if (exact) { + return extPk.derivationPath === path && extPk.curve === curve; + } + return path.startsWith(`${extPk.derivationPath}/`) && extPk.curve === curve; + }; + +export function hasPublicKeyFor( + secrets: PrimaryWalletSecrets, + path: string, + curve: Curve, +): boolean { + return secrets.publicKeys.some(findPublicKey(path, curve)); +} + +export function getPublicKeyFor( + secrets: PrimaryWalletSecrets, + path: string, + curve: Curve, +): AddressPublicKeyJson | undefined { + return secrets.publicKeys.find(findPublicKey(path, curve)); +} + +export function hasExtendedPublicKeyFor( + keys: ExtendedPublicKey[], + path: string, + curve: Curve, +): boolean { + return keys.some(findExtendedPublicKey(path, curve)); +} + +export function getExtendedPublicKeyFor( + keys: ExtendedPublicKey[], + path: string, + curve: Curve, +): ExtendedPublicKey | undefined { + return keys.find(findExtendedPublicKey(path, curve)); +} + +export function getExtendedPublicKey( + keys: ExtendedPublicKey[], + path: string, + curve: Curve, +): ExtendedPublicKey | undefined { + return keys.find(findExtendedPublicKey(path, curve, true)); +} diff --git a/src/background/services/seedless/SeedlessWallet.test.ts b/src/background/services/seedless/SeedlessWallet.test.ts index 4e56ed152..ddc8db15b 100644 --- a/src/background/services/seedless/SeedlessWallet.test.ts +++ b/src/background/services/seedless/SeedlessWallet.test.ts @@ -41,6 +41,7 @@ import { SeedlessSessionManager } from './SeedlessSessionManager'; import { SeedlessMfaService } from './SeedlessMfaService'; import { MfaRequestType } from './models'; import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; +import { AddressPublicKey } from '../secrets/AddressPublicKey'; jest.mock('@cubist-labs/cubesigner-sdk'); jest.mock('@cubist-labs/cubesigner-sdk-ethers-v6'); @@ -164,10 +165,16 @@ describe('src/background/services/seedless/SeedlessWallet', () => { it('correctly extracts the public keys', async () => { expect(await wallet.getPublicKeys()).toEqual([ - { - evm: strip0x(evmKey.publicKey), - xp: avaKey.publicKey, - }, + AddressPublicKey.fromJSON({ + key: strip0x(evmKey.publicKey), + derivationPath: evmKey.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), + AddressPublicKey.fromJSON({ + key: strip0x(avaKey.publicKey), + derivationPath: avaKey.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), ]); }); }); @@ -183,14 +190,26 @@ describe('src/background/services/seedless/SeedlessWallet', () => { it(`sorts them by derivation path's account index`, async () => { expect(await wallet.getPublicKeys()).toEqual([ - { - evm: strip0x(evmKey.publicKey), - xp: avaKey.publicKey, - }, - { - evm: evmKey2.publicKey, - xp: avaKey2.publicKey, - }, + AddressPublicKey.fromJSON({ + key: strip0x(evmKey.publicKey), + derivationPath: evmKey.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), + AddressPublicKey.fromJSON({ + key: strip0x(avaKey.publicKey), + derivationPath: avaKey.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), + AddressPublicKey.fromJSON({ + key: strip0x(evmKey2.publicKey), + derivationPath: evmKey2.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), + AddressPublicKey.fromJSON({ + key: strip0x(avaKey2.publicKey), + derivationPath: avaKey2.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), ]); }); }); @@ -212,10 +231,16 @@ describe('src/background/services/seedless/SeedlessWallet', () => { it('extracts the public keys from the first valid set', async () => { expect(await wallet.getPublicKeys()).toEqual([ - { - evm: anotherValidEvmKey.publicKey, - xp: anotherValidAvaKey.publicKey, - }, + AddressPublicKey.fromJSON({ + key: strip0x(anotherValidEvmKey.publicKey), + derivationPath: anotherValidEvmKey.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), + AddressPublicKey.fromJSON({ + key: strip0x(anotherValidAvaKey.publicKey), + derivationPath: anotherValidAvaKey.derivation_info.derivation_path, + curve: 'secp256k1', + }).toJSON(), ]); }); }); @@ -246,8 +271,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: 'la la la', - }, + key: 'la la la', + } as any, }); }); @@ -265,8 +290,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: 'la la la', - }, + key: 'la la la', + } as any, network: {} as any, }); }); @@ -298,8 +323,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: evmKey.publicKey, - }, + key: 'la la la', + } as any, network: { vmName: NetworkVMType.EVM } as any, sessionManager, }); @@ -398,9 +423,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - xp: 'xp xp xp', - }, + key: strip0x(evmKey.publicKey), + } as any, network: {} as any, sessionManager, }); @@ -468,9 +492,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - xp: strip0x(avaKey.publicKey), - }, + key: strip0x(avaKey.publicKey), + } as any, network: {} as any, sessionManager, }); @@ -597,8 +620,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - }, + key: strip0x(evmKey.publicKey), + } as any, network: {} as any, sessionManager, }); @@ -715,9 +738,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - xp: 'xp xp xp', - }, + key: strip0x(evmKey.publicKey), + } as any, network: {} as any, }); }); @@ -727,8 +749,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - }, + key: '', + } as any, network: {} as any, }); @@ -765,8 +787,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - }, + key: strip0x(evmKey.publicKey), + } as any, network: {} as any, }); @@ -803,8 +825,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - }, + key: strip0x(evmKey.publicKey), + } as any, }); global.fetch = jest.fn().mockResolvedValue({ @@ -837,8 +859,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: 'unpaired-public-key', - }, + key: 'uknown key', + } as any, }); }); @@ -954,8 +976,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: 'la la la', - }, + key: strip0x(evmKey.publicKey), + } as any, network: { chainId: ChainId.BITCOIN } as any, }); }); @@ -990,7 +1012,7 @@ describe('src/background/services/seedless/SeedlessWallet', () => { }); describe('when all requirements are met', () => { - const pubKey = { evm: btcKey.publicKey }; + const pubKey = { key: btcKey.publicKey } as any; const network: any = { chainId: ChainId.BITCOIN, }; @@ -1032,7 +1054,7 @@ describe('src/background/services/seedless/SeedlessWallet', () => { inputs.forEach((_, i) => { expect(SeedlessBtcSigner).toHaveBeenNthCalledWith( i + 1, - pubKey.evm, + pubKey.key, psbt, i, inputs, @@ -1111,8 +1133,8 @@ describe('src/background/services/seedless/SeedlessWallet', () => { networkService, sessionStorage, addressPublicKey: { - evm: strip0x(evmKey.publicKey), - }, + key: strip0x(evmKey.publicKey), + } as any, mfaService, }); }); diff --git a/src/background/services/seedless/SeedlessWallet.ts b/src/background/services/seedless/SeedlessWallet.ts index 4c5e6faee..182ada920 100644 --- a/src/background/services/seedless/SeedlessWallet.ts +++ b/src/background/services/seedless/SeedlessWallet.ts @@ -25,7 +25,6 @@ import { } from '@metamask/eth-sig-util'; import { NetworkService } from '../network/NetworkService'; -import { PubKeyType } from '../wallet/models'; import { MessageParams, MessageType } from '../messages/models'; import { SeedlessBtcSigner } from './SeedlessBtcSigner'; import { Transaction } from 'bitcoinjs-lib'; @@ -37,20 +36,23 @@ import { isTokenExpiredError } from './utils'; import { SeedlessMfaService } from './SeedlessMfaService'; import { toUtf8 } from 'ethereumjs-util'; import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; +import { AddressPublicKeyJson } from '../secrets/models'; +import { assertPresent } from '@src/utils/assertions'; +import { CommonError } from '@src/utils/errors'; type ConstructorOpts = { - networkService: NetworkService; + networkService?: NetworkService; sessionStorage: cs.SessionStorage; - addressPublicKey?: PubKeyType; + addressPublicKey?: AddressPublicKeyJson; network?: Network; sessionManager?: SeedlessSessionManager; mfaService?: SeedlessMfaService; }; export class SeedlessWallet { - #networkService: NetworkService; #sessionStorage: cs.SessionStorage; - #addressPublicKey?: PubKeyType; + #networkService?: NetworkService; + #addressPublicKey?: AddressPublicKeyJson; #network?: Network; #sessionManager?: SeedlessSessionManager; #signerSession?: cs.SignerSession; @@ -139,7 +141,7 @@ export class SeedlessWallet { const keys = await session.keys(); const activeAccountKey = keys.find( - (key) => strip0x(key.publicKey) === this.#addressPublicKey?.evm, + (key) => strip0x(key.publicKey) === this.#addressPublicKey?.key, ); const mnemonicId = activeAccountKey?.derivation_info?.mnemonic_id; @@ -250,7 +252,7 @@ export class SeedlessWallet { return result.data(); } - async getPublicKeys(): Promise { + async getPublicKeys(): Promise { const session = await this.#getSession(); // get keys and filter out non derived ones and group them let rawKeys: cs.KeyInfo[]; @@ -314,17 +316,34 @@ export class SeedlessWallet { // If there are multiple valid sets, we choose the first one. const derivedKeys = validKeySets[0]; - const pubkeys = [] as PubKeyType[]; + const pubkeys = [] as AddressPublicKeyJson[]; derivedKeys.forEach((key) => { if (!key || !key['SecpAvaAddr'] || !key['SecpEthAddr']) { return; } - pubkeys.push({ - evm: strip0x(key['SecpEthAddr'].public_key), - xp: strip0x(key['SecpAvaAddr'].public_key), - }); + if ( + !key['SecpEthAddr'].derivation_info?.derivation_path || + !key['SecpAvaAddr'].derivation_info?.derivation_path + ) { + throw new Error('Derivation path not found'); + } + + pubkeys.push( + { + curve: 'secp256k1', + derivationPath: key['SecpEthAddr'].derivation_info.derivation_path, + key: strip0x(key['SecpEthAddr'].public_key), + type: 'address-pubkey', + }, + { + curve: 'secp256k1', + derivationPath: key['SecpAvaAddr'].derivation_info.derivation_path, + key: strip0x(key['SecpAvaAddr'].public_key), + type: 'address-pubkey', + }, + ); }); if (!pubkeys?.length) { @@ -361,7 +380,7 @@ export class SeedlessWallet { } async signTransaction(transaction: TransactionRequest): Promise { - if (!this.#addressPublicKey || !this.#addressPublicKey.evm) { + if (!this.#addressPublicKey || !this.#addressPublicKey.key) { throw new Error('Public key not available'); } @@ -376,7 +395,7 @@ export class SeedlessWallet { try { const signer = new Signer( - getEvmAddressFromPubKey(Buffer.from(this.#addressPublicKey.evm, 'hex')), + getEvmAddressFromPubKey(Buffer.from(this.#addressPublicKey.key, 'hex')), await this.#getSession(), provider, ); @@ -391,6 +410,8 @@ export class SeedlessWallet { async signAvalancheTx( request: Avalanche.SignTxRequest, ): Promise { + assertPresent(this.#networkService, CommonError.UnknownNetwork); + if (!this.#addressPublicKey) { throw new Error('Public key not available'); } @@ -399,10 +420,10 @@ export class SeedlessWallet { const isMainnet = this.#networkService.isMainnet(); const session = await this.#getSession(); const key = isEvmTx - ? await this.#getSigningKey(cs.Secp256k1.Evm, this.#addressPublicKey.evm) + ? await this.#getSigningKey(cs.Secp256k1.Evm, this.#addressPublicKey.key) : await this.#getSigningKey( isMainnet ? cs.Secp256k1.Ava : cs.Secp256k1.AvaTest, - this.#addressPublicKey.xp, + this.#addressPublicKey.key, ); try { @@ -448,7 +469,7 @@ export class SeedlessWallet { } const signer = new SeedlessBtcSigner( - this.#addressPublicKey.evm, + this.#addressPublicKey.key, psbt, i, ins, @@ -473,6 +494,7 @@ export class SeedlessWallet { messageType: MessageType, messageParams: MessageParams, ): Promise { + assertPresent(this.#networkService, CommonError.UnknownNetwork); // TODO: is networkService actually needed? why is #network not enough? if (!this.#addressPublicKey) { throw new Error('Public key not available'); } @@ -482,13 +504,13 @@ export class SeedlessWallet { } if (messageType === MessageType.AVALANCHE_SIGN) { - if (!this.#addressPublicKey.xp) { + if (!this.#addressPublicKey.key) { throw new Error('X/P public key not available'); } const xpProvider = await this.#networkService.getAvalanceProviderXP(); const addressAVM = await xpProvider - .getAddress(Buffer.from(this.#addressPublicKey.xp, 'hex'), 'X') + .getAddress(Buffer.from(this.#addressPublicKey.key, 'hex'), 'X') .slice(2); // remove chain prefix const message = toUtf8(messageParams.data); @@ -504,7 +526,7 @@ export class SeedlessWallet { } const addressEVM = getEvmAddressFromPubKey( - Buffer.from(this.#addressPublicKey.evm, 'hex'), + Buffer.from(this.#addressPublicKey.key, 'hex'), ).toLowerCase(); switch (messageType) { diff --git a/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.test.ts b/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.test.ts index b925371fe..b675ffc47 100644 --- a/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.test.ts +++ b/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.test.ts @@ -46,12 +46,7 @@ describe('src/background/services/seedless/handlers/cancelRecoveryPhraseExport', secretsService.getPrimaryAccountSecrets.mockResolvedValue({ secretType: SecretType.Seedless, - pubKeys: [ - { - evm: 'evm', - xp: 'xp', - }, - ], + publicKeys: [{ key: 'evm' }], } as any); }); diff --git a/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.ts b/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.ts index 2d026b376..a3b368835 100644 --- a/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.ts +++ b/src/background/services/seedless/handlers/cancelRecoveryPhraseExport.ts @@ -42,7 +42,7 @@ export class CancelRecoveryPhraseExportHandler implements HandlerType { const wallet = new SeedlessWallet({ networkService: this.networkService, sessionStorage: new SeedlessTokenStorage(this.secretsService), - addressPublicKey: secrets.pubKeys[0], + addressPublicKey: secrets.publicKeys[0], mfaService: this.seedlessMfaService, }); diff --git a/src/background/services/seedless/handlers/completeRecoveryPhraseExport.test.ts b/src/background/services/seedless/handlers/completeRecoveryPhraseExport.test.ts index 7b261d9ab..25f0935dc 100644 --- a/src/background/services/seedless/handlers/completeRecoveryPhraseExport.test.ts +++ b/src/background/services/seedless/handlers/completeRecoveryPhraseExport.test.ts @@ -56,12 +56,7 @@ describe('src/background/services/seedless/handlers/completeRecoveryPhraseExport secretsService.getPrimaryAccountSecrets.mockResolvedValue({ secretType: SecretType.Seedless, - pubKeys: [ - { - evm: 'evm', - xp: 'xp', - }, - ], + publicKeys: [{ key: 'evm' }], } as any); }); diff --git a/src/background/services/seedless/handlers/completeRecoveryPhraseExport.ts b/src/background/services/seedless/handlers/completeRecoveryPhraseExport.ts index 84d109589..59e105f4e 100644 --- a/src/background/services/seedless/handlers/completeRecoveryPhraseExport.ts +++ b/src/background/services/seedless/handlers/completeRecoveryPhraseExport.ts @@ -49,7 +49,7 @@ export class CompleteRecoveryPhraseExportHandler implements HandlerType { const wallet = new SeedlessWallet({ networkService: this.networkService, sessionStorage: new SeedlessTokenStorage(this.secretsService), - addressPublicKey: secrets.pubKeys[0], + addressPublicKey: secrets.publicKeys[0], mfaService: this.seedlessMfaService, }); diff --git a/src/background/services/seedless/handlers/getRecoveryPhraseExportState.test.ts b/src/background/services/seedless/handlers/getRecoveryPhraseExportState.test.ts index 08fb2635c..d772450c3 100644 --- a/src/background/services/seedless/handlers/getRecoveryPhraseExportState.test.ts +++ b/src/background/services/seedless/handlers/getRecoveryPhraseExportState.test.ts @@ -46,12 +46,7 @@ describe('src/background/services/seedless/handlers/ge', () => { secretsService.getPrimaryAccountSecrets.mockResolvedValue({ secretType: SecretType.Seedless, - pubKeys: [ - { - evm: 'evm', - xp: 'xp', - }, - ], + publicKeys: [{ key: 'evm' }], } as any); }); diff --git a/src/background/services/seedless/handlers/getRecoveryPhraseExportState.ts b/src/background/services/seedless/handlers/getRecoveryPhraseExportState.ts index bc24e4443..dca74b448 100644 --- a/src/background/services/seedless/handlers/getRecoveryPhraseExportState.ts +++ b/src/background/services/seedless/handlers/getRecoveryPhraseExportState.ts @@ -44,7 +44,7 @@ export class GetRecoveryPhraseExportStateHandler implements HandlerType { const wallet = new SeedlessWallet({ networkService: this.networkService, sessionStorage: new SeedlessTokenStorage(this.secretsService), - addressPublicKey: secrets.pubKeys[0], + addressPublicKey: secrets.publicKeys[0], mfaService: this.seedlessMfaService, }); diff --git a/src/background/services/seedless/handlers/initRecoveryPhraseExport.test.ts b/src/background/services/seedless/handlers/initRecoveryPhraseExport.test.ts index ce3114776..64405de18 100644 --- a/src/background/services/seedless/handlers/initRecoveryPhraseExport.test.ts +++ b/src/background/services/seedless/handlers/initRecoveryPhraseExport.test.ts @@ -47,12 +47,7 @@ describe('src/background/services/seedless/handlers/initRecoveryPhraseExport', ( secretsService.getPrimaryAccountSecrets.mockResolvedValue({ secretType: SecretType.Seedless, - pubKeys: [ - { - evm: 'evm', - xp: 'xp', - }, - ], + publicKeys: [{ key: 'evm' }], } as any); }); diff --git a/src/background/services/seedless/handlers/initRecoveryPhraseExport.ts b/src/background/services/seedless/handlers/initRecoveryPhraseExport.ts index 043bb84a1..d855f163f 100644 --- a/src/background/services/seedless/handlers/initRecoveryPhraseExport.ts +++ b/src/background/services/seedless/handlers/initRecoveryPhraseExport.ts @@ -44,7 +44,7 @@ export class InitRecoveryPhraseExportHandler implements HandlerType { const wallet = new SeedlessWallet({ networkService: this.networkService, sessionStorage: new SeedlessTokenStorage(this.secretsService), - addressPublicKey: secrets.pubKeys[0], + addressPublicKey: secrets.publicKeys[0], mfaService: this.seedlessMfaService, }); diff --git a/src/background/services/storage/StorageService.test.ts b/src/background/services/storage/StorageService.test.ts index 04b27680a..76b1ef630 100644 --- a/src/background/services/storage/StorageService.test.ts +++ b/src/background/services/storage/StorageService.test.ts @@ -392,7 +392,14 @@ describe('src/background/services/storage/StorageService.ts', () => { 'some-key', 'some-data', expect.anything(), + expect.any(Function), ); + + const dependencyLoader = migrationSpy.mock.calls[0]?.[3]; + + const loadSpy = jest.spyOn(service, 'load'); + dependencyLoader?.('some-dependency'); + expect(loadSpy).toHaveBeenCalledWith('some-dependency', 'some-password'); }); it('decrypts data with encryption key', async () => { diff --git a/src/background/services/storage/StorageService.ts b/src/background/services/storage/StorageService.ts index 58f76c9c3..2ae445ba4 100644 --- a/src/background/services/storage/StorageService.ts +++ b/src/background/services/storage/StorageService.ts @@ -170,9 +170,13 @@ export class StorageService implements OnLock { const deserializedData = deserializeFromJSON(data); if (deserializedData) { - return migrateToLatest(key, deserializedData, (migratedData) => - this.save(key, migratedData, customEncryptionKey), - ); + return migrateToLatest( + key, + deserializedData, + (migratedData) => this.save(key, migratedData, customEncryptionKey), + (dependencyKey: string) => + this.load(dependencyKey, customEncryptionKey), + ) as T; } return deserializedData; diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v4/utils/getSecretsType.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v4/utils/getSecretsType.ts index 69fe9018c..857efbe7d 100644 --- a/src/background/services/storage/schemaMigrations/migrations/wallet_v4/utils/getSecretsType.ts +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v4/utils/getSecretsType.ts @@ -8,6 +8,7 @@ export type WalletKeys = { /** * Public keys used for X/P chain are from a different derivation path. */ + sol?: string; xp?: string; btcWalletPolicyDetails?: { hmacHex: string; diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonModels.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonModels.ts new file mode 100644 index 000000000..126d59854 --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonModels.ts @@ -0,0 +1,38 @@ +export type BtcWalletPolicyDetails = { + hmacHex: string; + /** + * Extended public key of m/44'/60'/n + */ + xpub: string; + masterFingerprint: string; + name: string; +}; + +export type PubKeyType = { + evm: string; + /** + * Public keys used for X/P chain are from a different derivation path. + */ + xp?: string; + btcWalletPolicyDetails?: BtcWalletPolicyDetails; + ed25519?: string; +}; + +export type ImportedPrivateKeySecrets = { + secretType: 'private-key'; + secret: string; +}; + +export type ImportedWalletConnectSecrets = { + secretType: 'wallet-connect'; + pubKey?: PubKeyType; +}; + +export type ImportedFireblocksSecrets = { + secretType: 'fireblocks'; + api?: { + vaultAccountId: string; + key: string; + secret: string; + }; +}; diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonSchemas.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonSchemas.ts new file mode 100644 index 000000000..9ec146c81 --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/commonSchemas.ts @@ -0,0 +1,48 @@ +import Joi from 'joi'; + +import { + BtcWalletPolicyDetails, + ImportedFireblocksSecrets, + ImportedPrivateKeySecrets, + ImportedWalletConnectSecrets, + PubKeyType, +} from './commonModels'; + +export const btcWalletPolicyDetailsSchema = Joi.object< + BtcWalletPolicyDetails, + true +>({ + hmacHex: Joi.string().required(), + masterFingerprint: Joi.string().required(), + name: Joi.string().required(), + xpub: Joi.string().required(), +}); + +export const pubKeyTypeSchema = Joi.object({ + evm: Joi.string().required(), + xp: Joi.string().optional(), + ed25519: Joi.string().optional(), + btcWalletPolicyDetails: btcWalletPolicyDetailsSchema.optional(), +}).unknown(); + +export const walletConnectSchema = Joi.object< + ImportedWalletConnectSecrets, + true +>({ + secretType: Joi.string().valid('wallet-connect').required(), + pubKey: pubKeyTypeSchema.optional(), +}).unknown(); + +export const fireblocksSchema = Joi.object({ + secretType: Joi.string().valid('fireblocks').required(), + api: Joi.object({ + vaultAccountId: Joi.string().required(), + key: Joi.string().required(), + secret: Joi.string().required(), + }).optional(), +}).unknown(); + +export const privateKeySchema = Joi.object({ + secretType: Joi.string().valid('private-key').required(), + secret: Joi.string().required(), +}).unknown(); diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacyModels.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacyModels.ts new file mode 100644 index 000000000..2c67ea7be --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacyModels.ts @@ -0,0 +1,75 @@ +import { ImportedPrivateKeyAccount } from '@src/background/services/accounts/models'; +import { + BtcWalletPolicyDetails, + ImportedFireblocksSecrets, + ImportedWalletConnectSecrets, + PubKeyType, +} from './commonModels'; + +export type SeedlessSecrets = { + id: string; + name: string; + secretType: 'seedless'; + pubKeys: PubKeyType[]; + derivationPath: 'bip44'; + seedlessSignerToken?: any; // external type + authProvider: 'google' | 'apple'; + userEmail?: string; + userId?: string; +}; + +export type MnemonicSecrets = { + id: string; + name: string; + secretType: 'mnemonic'; + mnemonic: string; + xpub: string; + xpubXP: string; + derivationPath: 'bip44'; +}; + +export type KeystoneSecrets = { + id: string; + name: string; + secretType: 'keystone'; + masterFingerprint: string; + xpub: string; + derivationPath: 'bip44'; +}; + +export type LedgerSecrets = { + id: string; + name: string; + secretType: 'ledger'; + xpub: string; + xpubXP?: string; + derivationPath: 'bip44'; + btcWalletPolicyDetails?: BtcWalletPolicyDetails; +}; + +export type LedgerLiveSecrets = { + id: string; + name: string; + secretType: 'ledger-live'; + pubKeys: PubKeyType[]; + derivationPath: 'ledger_live'; +}; + +export type LegacySchema = { + wallets: Array< + | KeystoneSecrets + | LedgerLiveSecrets + | LedgerSecrets + | MnemonicSecrets + | SeedlessSecrets + >; + importedAccounts: Record< + string, + Array< + | ImportedPrivateKeyAccount + | ImportedWalletConnectSecrets + | ImportedFireblocksSecrets + > + >; + version: 4; +}; diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacySchema.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacySchema.ts new file mode 100644 index 000000000..49bf34bfe --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/legacySchema.ts @@ -0,0 +1,83 @@ +import Joi from 'joi'; + +import * as Legacy from './legacyModels'; +import { + btcWalletPolicyDetailsSchema, + fireblocksSchema, + pubKeyTypeSchema, + walletConnectSchema, +} from './commonSchemas'; +import { ImportedPrivateKeySecrets } from './commonModels'; + +const seedlessSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('seedless').required(), + derivationPath: Joi.string().valid('bip44').required(), + pubKeys: Joi.array().items(pubKeyTypeSchema).required(), + authProvider: Joi.string().valid('google', 'apple').required(), + seedlessSignerToken: Joi.object().optional(), + userEmail: Joi.string().optional(), + userId: Joi.string().optional(), +}).unknown(); + +const mnemonicSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('mnemonic').required(), + derivationPath: Joi.string().valid('bip44').required(), + mnemonic: Joi.string().required(), + xpub: Joi.string().required(), + xpubXP: Joi.string().required(), +}).unknown(); + +const ledgerSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('ledger').required(), + derivationPath: Joi.string().valid('bip44').required(), + xpub: Joi.string().required(), + xpubXP: Joi.string().optional(), + btcWalletPolicyDetails: btcWalletPolicyDetailsSchema.optional(), +}).unknown(); + +const ledgerLiveSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('ledger-live').required(), + derivationPath: Joi.string().valid('ledger_live').required(), + pubKeys: Joi.array().items(pubKeyTypeSchema).required(), +}).unknown(); + +const keystoneSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('keystone').required(), + derivationPath: Joi.string().valid('bip44').required(), + xpub: Joi.string().required(), + masterFingerprint: Joi.string().required(), +}).unknown(); + +const privateKeySchema = Joi.object({ + secretType: Joi.string().valid('private-key').required(), + secret: Joi.string().required(), +}).unknown(); + +export const legacySecretsSchema = Joi.object({ + wallets: Joi.array() + .items( + keystoneSchema, + ledgerLiveSchema, + ledgerSchema, + mnemonicSchema, + seedlessSchema, + ) + .required(), + importedAccounts: Joi.object() + .pattern( + Joi.string(), + Joi.alternatives(privateKeySchema, walletConnectSchema, fireblocksSchema), + ) + .optional(), + version: Joi.number().valid(4).required(), +}).unknown(); diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v5/newModels.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/newModels.ts new file mode 100644 index 000000000..2c550685e --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/newModels.ts @@ -0,0 +1,91 @@ +import { ImportedPrivateKeyAccount } from '@src/background/services/accounts/models'; +import { + BtcWalletPolicyDetails, + ImportedFireblocksSecrets, + ImportedWalletConnectSecrets, +} from './commonModels'; + +export type AddressPublicKey = { + type: 'address-pubkey'; + curve: 'secp256k1' | 'ed25519'; + derivationPath: string; + key: string; + btcWalletPolicyDetails?: BtcWalletPolicyDetails; +}; + +export type DerivationPathSpec = 'bip44' | 'ledger_live'; +export type ExtendedPublicKey = { + type: 'extended-pubkey'; + curve: 'secp256k1'; + derivationPath: string; + key: string; + btcWalletPolicyDetails?: BtcWalletPolicyDetails; +}; + +export type SeedlessSecrets = { + id: string; + name: string; + secretType: 'seedless'; + publicKeys: AddressPublicKey[]; + derivationPathSpec: 'bip44'; + seedlessSignerToken: any; // external type + authProvider: 'google' | 'apple'; + userEmail?: string; + userId?: string; +}; + +export type MnemonicSecrets = { + id: string; + name: string; + secretType: 'mnemonic'; + mnemonic: string; + extendedPublicKeys: ExtendedPublicKey[]; + publicKeys: AddressPublicKey[]; + derivationPathSpec: 'bip44'; +}; + +export type KeystoneSecrets = { + id: string; + name: string; + secretType: 'keystone'; + masterFingerprint: string; + publicKeys: AddressPublicKey[]; + extendedPublicKeys: ExtendedPublicKey[]; + derivationPathSpec: 'bip44'; +}; + +export type LedgerSecrets = { + id: string; + name: string; + secretType: 'ledger'; + publicKeys: AddressPublicKey[]; + extendedPublicKeys: ExtendedPublicKey[]; + derivationPathSpec: 'bip44'; +}; + +export type LedgerLiveSecrets = { + id: string; + name: string; + secretType: 'ledger-live'; + publicKeys: AddressPublicKey[]; + derivationPathSpec: 'ledger_live'; +}; + +export type NewSchema = { + wallets: Array< + | KeystoneSecrets + | LedgerLiveSecrets + | LedgerSecrets + | MnemonicSecrets + | SeedlessSecrets + >; + importedAccounts: Record< + string, + Array< + | ImportedPrivateKeyAccount + | ImportedWalletConnectSecrets + | ImportedFireblocksSecrets + > + >; + version: 5; +}; diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v5/newSchema.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/newSchema.ts new file mode 100644 index 000000000..551b3ecf8 --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/newSchema.ts @@ -0,0 +1,93 @@ +import Joi from 'joi'; + +import { + btcWalletPolicyDetailsSchema, + fireblocksSchema, + privateKeySchema, + walletConnectSchema, +} from './commonSchemas'; +import * as New from './newModels'; + +const addressPublicKeySchema = Joi.object({ + type: Joi.string().valid('address-pubkey').required(), + curve: Joi.string().valid('secp256k1', 'ed25519').required(), + derivationPath: Joi.string().required(), + key: Joi.string().required(), + btcWalletPolicyDetails: btcWalletPolicyDetailsSchema.optional(), +}); + +const extendedPublicKeySchema = Joi.object({ + type: Joi.string().valid('extended-pubkey').required(), + curve: Joi.string().valid('secp256k1').required(), + derivationPath: Joi.string().required(), + key: Joi.string().required(), + btcWalletPolicyDetails: btcWalletPolicyDetailsSchema.optional(), +}); + +const seedlessSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('seedless').required(), + derivationPathSpec: Joi.string().valid('bip44').required(), + publicKeys: Joi.array().items(addressPublicKeySchema).required(), + authProvider: Joi.string().valid('google', 'apple').required(), + seedlessSignerToken: Joi.object().optional(), + userEmail: Joi.string().optional(), + userId: Joi.string().optional(), +}).unknown(); + +const mnemonicSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('mnemonic').required(), + derivationPathSpec: Joi.string().valid('bip44').required(), + mnemonic: Joi.string().required(), + publicKeys: Joi.array().items(addressPublicKeySchema).required(), + extendedPublicKeys: Joi.array().items(extendedPublicKeySchema).required(), +}).unknown(); + +const ledgerSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('ledger').required(), + derivationPathSpec: Joi.string().valid('bip44').required(), + publicKeys: Joi.array().items(addressPublicKeySchema).required(), + extendedPublicKeys: Joi.array().items(extendedPublicKeySchema).required(), +}).unknown(); + +const ledgerLiveSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('ledger-live').required(), + derivationPathSpec: Joi.string().valid('ledger_live').required(), + publicKeys: Joi.array().items(addressPublicKeySchema).required(), +}).unknown(); + +const keystoneSchema = Joi.object({ + id: Joi.string().required(), + name: Joi.string().optional(), + secretType: Joi.string().valid('keystone').required(), + derivationPathSpec: Joi.string().valid('bip44').required(), + masterFingerprint: Joi.string().required(), + publicKeys: Joi.array().items(addressPublicKeySchema).required(), + extendedPublicKeys: Joi.array().items(extendedPublicKeySchema).required(), +}).unknown(); + +export const newSecretsSchema = Joi.object({ + wallets: Joi.array() + .items( + keystoneSchema, + ledgerLiveSchema, + ledgerSchema, + mnemonicSchema, + seedlessSchema, + ) + .required(), + importedAccounts: Joi.object() + .pattern( + Joi.string(), + Joi.alternatives(privateKeySchema, walletConnectSchema, fireblocksSchema), + ) + .optional(), + version: Joi.number().valid(5).required(), +}).unknown(); diff --git a/src/background/services/storage/schemaMigrations/migrations/wallet_v5/wallet_v5.ts b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/wallet_v5.ts new file mode 100644 index 000000000..7476f34d0 --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/wallet_v5/wallet_v5.ts @@ -0,0 +1,310 @@ +import { + ACCOUNTS_STORAGE_KEY, + Accounts, +} from '@src/background/services/accounts/models'; + +import { MigrationWithDeps } from '../../models'; + +import { legacySecretsSchema } from './legacySchema'; +import * as Legacy from './legacyModels'; +import * as New from './newModels'; +import { + AddressPublicKeyJson, + ExtendedPublicKey, +} from '@src/background/services/secrets/models'; +import { assertPresent } from '@src/utils/assertions'; +import { CommonError } from '@src/utils/errors'; +import { AddressPublicKey } from '@src/background/services/secrets/AddressPublicKey'; + +const VERSION = 5; +const EVM_BASE_PATH = "m/44'/60'/0'"; +const AVALANCHE_BASE_PATH = "m/44'/9000'/0'"; + +type DependencyModelTuples = [[typeof ACCOUNTS_STORAGE_KEY, Accounts]]; + +type WalletV5Migration = MigrationWithDeps< + Legacy.LegacySchema, + New.NewSchema, + DependencyModelTuples +>; + +const migrateMnemonicSecrets = async ( + secrets: Legacy.MnemonicSecrets, + numberOfAccounts: number, +): Promise => { + const walletId = secrets.id; + const addressPublicKeys: AddressPublicKeyJson[] = []; + + const extendedPublicKeys: ExtendedPublicKey[] = [ + { + // xpub -> ExtendedPublicKey + curve: 'secp256k1', + derivationPath: EVM_BASE_PATH, + key: secrets.xpub, + type: 'extended-pubkey', + }, + { + // xpubXP -> ExtendedPublicKey + curve: 'secp256k1', + derivationPath: AVALANCHE_BASE_PATH, + key: secrets.xpubXP, + type: 'extended-pubkey', + }, + ]; + + for (let i = 0; i < numberOfAccounts; i++) { + const evmPublicKey = await AddressPublicKey.fromSeedphrase( + secrets.mnemonic, + 'secp256k1', + `${EVM_BASE_PATH}/0/${i}`, + ); + const avaSecp256k1PublicKey = await AddressPublicKey.fromSeedphrase( + secrets.mnemonic, + 'secp256k1', + `${AVALANCHE_BASE_PATH}/0/${i}`, + ); + const avaEd25519PublicKey = await AddressPublicKey.fromSeedphrase( + secrets.mnemonic, + 'ed25519', + `${AVALANCHE_BASE_PATH}/0'/${i}'`, // Ed25519 requires hardened path + ); + + addressPublicKeys.push( + evmPublicKey.toJSON(), + avaSecp256k1PublicKey.toJSON(), + avaEd25519PublicKey.toJSON(), + ); + } + + return { + id: walletId, + name: secrets.name, + derivationPathSpec: 'bip44', + extendedPublicKeys, + mnemonic: secrets.mnemonic, + publicKeys: addressPublicKeys, + secretType: 'mnemonic', + }; +}; + +const migrateLedgerSecrets = ( + secrets: Legacy.LedgerSecrets, + numberOfAccounts: number, +): New.LedgerSecrets => { + const walletId = secrets.id; + const addressPublicKeys: AddressPublicKeyJson[] = []; + + const extendedPublicKeys: ExtendedPublicKey[] = [ + { + // xpub -> ExtendedPublicKey + curve: 'secp256k1', + derivationPath: EVM_BASE_PATH, + key: secrets.xpub, + type: 'extended-pubkey', + }, + ]; + + if (secrets.xpubXP) { + extendedPublicKeys.push({ + // xpubXP -> ExtendedPublicKey + curve: 'secp256k1', + derivationPath: AVALANCHE_BASE_PATH, + key: secrets.xpubXP, + type: 'extended-pubkey', + btcWalletPolicyDetails: secrets.btcWalletPolicyDetails, + }); + } + + for (let i = 0; i < numberOfAccounts; i++) { + const evmPublicKey = AddressPublicKey.fromExtendedPublicKeys( + extendedPublicKeys, + 'secp256k1', + `${EVM_BASE_PATH}/0/${i}`, + ); + const avaPublicKey = AddressPublicKey.fromExtendedPublicKeys( + extendedPublicKeys, + 'secp256k1', + `${AVALANCHE_BASE_PATH}/0/${i}`, + ); + + addressPublicKeys.push(evmPublicKey.toJSON(), avaPublicKey.toJSON()); + } + + return { + id: walletId, + name: secrets.name, + derivationPathSpec: 'bip44', + extendedPublicKeys, + publicKeys: addressPublicKeys, + secretType: 'ledger', + }; +}; + +const migrateLedgerLiveSecrets = ( + secrets: Legacy.LedgerLiveSecrets, +): New.LedgerLiveSecrets => { + const walletId = secrets.id; + const addressPublicKeys: AddressPublicKeyJson[] = []; + + for (const [index, legacyPubKey] of secrets.pubKeys.entries()) { + if (legacyPubKey.evm) { + addressPublicKeys.push({ + curve: 'secp256k1', + derivationPath: `m/44'/60'/${index}'/0/0`, + key: legacyPubKey.evm, + type: 'address-pubkey', + btcWalletPolicyDetails: legacyPubKey.btcWalletPolicyDetails, + }); + } + + if (legacyPubKey.xp) { + addressPublicKeys.push({ + curve: 'secp256k1', + derivationPath: `m/44'/9000'/${index}'/0/0`, + key: legacyPubKey.xp, + type: 'address-pubkey', + }); + } + } + + return { + id: walletId, + name: secrets.name, + derivationPathSpec: 'ledger_live', + publicKeys: addressPublicKeys, + secretType: 'ledger-live', + }; +}; + +const migrateKeystoneSecrets = ( + secrets: Legacy.KeystoneSecrets, + numberOfAccounts: number, +): New.KeystoneSecrets => { + const walletId = secrets.id; + const addressPublicKeys: AddressPublicKeyJson[] = []; + + const extendedPublicKeys: ExtendedPublicKey[] = [ + { + // xpub -> ExtendedPublicKey + curve: 'secp256k1', + derivationPath: EVM_BASE_PATH, + key: secrets.xpub, + type: 'extended-pubkey', + }, + ]; + + for (let i = 0; i < numberOfAccounts; i++) { + const evmPublicKey = AddressPublicKey.fromExtendedPublicKeys( + extendedPublicKeys, + 'secp256k1', + `${EVM_BASE_PATH}/0/${i}`, + ); + + addressPublicKeys.push(evmPublicKey.toJSON()); + } + + return { + id: walletId, + name: secrets.name, + derivationPathSpec: 'bip44', + masterFingerprint: secrets.masterFingerprint, + extendedPublicKeys, + publicKeys: addressPublicKeys, + secretType: 'keystone', + }; +}; + +const migrateSeedlessSecrets = ( + secrets: Legacy.SeedlessSecrets, +): New.SeedlessSecrets => { + const walletId = secrets.id; + const addressPublicKeys: AddressPublicKeyJson[] = []; + + for (const [index, legacyPubKey] of secrets.pubKeys.entries()) { + if (legacyPubKey.evm) { + addressPublicKeys.push({ + curve: 'secp256k1', + derivationPath: `m/44'/60'/0'/0/${index}`, + key: legacyPubKey.evm, + type: 'address-pubkey', + btcWalletPolicyDetails: legacyPubKey.btcWalletPolicyDetails, + }); + } + + if (legacyPubKey.xp) { + addressPublicKeys.push({ + curve: 'secp256k1', + derivationPath: `m/44'/9000'/0'/0/${index}`, + key: legacyPubKey.xp, + type: 'address-pubkey', + }); + } + } + + return { + id: walletId, + name: secrets.name, + derivationPathSpec: 'bip44', + publicKeys: addressPublicKeys, + secretType: 'seedless', + authProvider: secrets.authProvider, + seedlessSignerToken: secrets.seedlessSignerToken, + userEmail: secrets.userEmail, + userId: secrets.userId, + }; +}; + +const up: WalletV5Migration['up'] = async (currentSecrets, accounts) => { + const primaryWallets: New.NewSchema['wallets'] = []; + + for (let i = 0; i < currentSecrets.wallets.length; i++) { + const wallet = currentSecrets.wallets[i]; + assertPresent(wallet, CommonError.MigrationFailed); + const { secretType } = wallet; + + const accountsForWallet = accounts.primary[wallet.id]; + assertPresent(accountsForWallet, CommonError.MigrationFailed); + + if (secretType === 'mnemonic') { + const mnemonicWallet = await migrateMnemonicSecrets( + wallet, + accountsForWallet.length, + ); + primaryWallets.push(mnemonicWallet); + } else if (secretType === 'ledger') { + const ledgerWallet = migrateLedgerSecrets( + wallet, + accountsForWallet.length, + ); + primaryWallets.push(ledgerWallet); + } else if (secretType === 'ledger-live') { + const ledgerLiveWallet = migrateLedgerLiveSecrets(wallet); + primaryWallets.push(ledgerLiveWallet); + } else if (secretType === 'keystone') { + const keystoneWallet = migrateKeystoneSecrets( + wallet, + accountsForWallet.length, + ); + primaryWallets.push(keystoneWallet); + } else if (secretType === 'seedless') { + const seedlessWallet = migrateSeedlessSecrets(wallet); + primaryWallets.push(seedlessWallet); + } + } + + return { + importedAccounts: currentSecrets.importedAccounts, + wallets: primaryWallets, + version: VERSION, + }; +}; + +export default { + previousSchema: legacySecretsSchema, + dependencyKeys: [ACCOUNTS_STORAGE_KEY], + up, +} satisfies MigrationWithDeps< + Legacy.LegacySchema, + New.NewSchema, + DependencyModelTuples +>; diff --git a/src/background/services/storage/schemaMigrations/models.ts b/src/background/services/storage/schemaMigrations/models.ts index 2e426f915..937706eb8 100644 --- a/src/background/services/storage/schemaMigrations/models.ts +++ b/src/background/services/storage/schemaMigrations/models.ts @@ -1 +1,44 @@ +import type Joi from 'joi'; + export const WALLET_ID = 'migrated-wallet-id'; + +type Dependencies = [string, unknown][]; +type KeysTuple = { [K in keyof T]: T[K][0] }; +type TypesTuple = { [K in keyof T]: T[K][1] }; + +export type SimpleMigration = { + previousSchema: Joi.Schema; + up: (data: PrevSchema) => Promise; +}; + +export type MigrationWithDeps< + PrevSchema, + NewSchema, + Deps extends Dependencies, +> = { + previousSchema: Joi.Schema; + dependencyKeys: KeysTuple; + up: ( + data: PrevSchema, + ...dependencies: TypesTuple + ) => Promise; +}; + +export type Migration< + PrevSchema = unknown, + NewSchema = unknown, + Deps extends [string, unknown][] = [string, unknown][], +> = + | SimpleMigration + | MigrationWithDeps; + +export type SchemaMap = Record< + string, + { + latestVersion: number; + migrations: readonly { + version: number; + migration: Migration; + }[]; + } +>; diff --git a/src/background/services/storage/schemaMigrations/schemaMap.ts b/src/background/services/storage/schemaMigrations/schemaMap.ts index dc31f03d3..dd001a63c 100644 --- a/src/background/services/storage/schemaMigrations/schemaMap.ts +++ b/src/background/services/storage/schemaMigrations/schemaMap.ts @@ -1,4 +1,3 @@ -import Joi from 'joi'; import { ACCOUNTS_STORAGE_KEY } from '../../accounts/models'; import { WALLET_STORAGE_ENCRYPTION_KEY } from '../models'; import { WALLET_STORAGE_KEY } from '@src/background/services/wallet/models'; @@ -19,22 +18,7 @@ import network_v4 from './migrations/network_v4'; import { UNIFIED_BRIDGE_STATE_STORAGE_KEY } from '../../unifiedBridge/models'; import unified_bridge_v2 from './migrations/unified_bridge_v2'; import balances_v3 from './migrations/balances_v3'; - -export type Migration = { - previousSchema: Joi.Schema; - up: (data: T) => Promise; -}; - -export type SchemaMap = Record< - string, - { - latestVersion: number; - migrations: readonly { - version: number; - migration: Migration; - }[]; - } ->; +import wallet_v5 from './migrations/wallet_v5/wallet_v5'; export const SCHEMA_MAP = { [ACCOUNTS_STORAGE_KEY]: { @@ -51,7 +35,7 @@ export const SCHEMA_MAP = { ], }, [WALLET_STORAGE_KEY]: { - latestVersion: 4, + latestVersion: 5, migrations: [ { version: 2, @@ -65,6 +49,10 @@ export const SCHEMA_MAP = { version: 4, migration: wallet_v4, }, + { + version: 5, + migration: wallet_v5, + }, ], }, [NETWORK_STORAGE_KEY]: { diff --git a/src/background/services/storage/schemaMigrations/schemaMigrations.ts b/src/background/services/storage/schemaMigrations/schemaMigrations.ts index 903df68dc..2389e84ff 100644 --- a/src/background/services/storage/schemaMigrations/schemaMigrations.ts +++ b/src/background/services/storage/schemaMigrations/schemaMigrations.ts @@ -1,4 +1,7 @@ -import { SchemaMap, SCHEMA_MAP } from './schemaMap'; +import { assertPresent } from '@src/utils/assertions'; +import { SchemaMap } from './models'; +import { SCHEMA_MAP } from './schemaMap'; +import { CommonError } from '@src/utils/errors'; export const getDataWithSchemaVersion = (key: string, data: T) => { if (Array.isArray(data) || !SCHEMA_MAP[key] || data?.['version']) { @@ -11,7 +14,8 @@ export const getDataWithSchemaVersion = (key: string, data: T) => { export const migrateToLatest = async ( key: keyof SchemaMap, data: T & { version?: number }, - onMigrationApplied?: (data: T) => Promise, + onMigrationApplied?: (data: unknown) => Promise, + loadDependency?: (key: string) => Promise, ): Promise => { const currentVersion = Array.isArray(data) ? 1 : (data.version ?? 1); const schema = SCHEMA_MAP[key] as SchemaMap[keyof SchemaMap] | undefined; @@ -25,7 +29,7 @@ export const migrateToLatest = async ( .filter(({ version }) => version > currentVersion) .sort((a, b) => a.version - b.version); - const results = await orderedMigrations.reduce>( + const results = await orderedMigrations.reduce( async (migrationResult, currentMigration) => { const result = await migrationResult; @@ -43,18 +47,28 @@ export const migrateToLatest = async ( ); } + if ('dependencyKeys' in currentMigration.migration) { + assertPresent(loadDependency, CommonError.Unknown); + + const dependencies = await Promise.all( + currentMigration.migration.dependencyKeys.map(loadDependency), + ); + + return currentMigration.migration.up(result, ...dependencies); + } + return currentMigration.migration.up(result); }, Promise.resolve( typeof data === 'object' && !Array.isArray(data) && data !== null ? { ...data, version: currentVersion } : data, - ), + ) as Promise<{ version?: number; [key: PropertyKey]: unknown }>, ); if (onMigrationApplied) { await onMigrationApplied(results); } - return results; + return results as T; }; diff --git a/src/background/services/wallet/WalletService.test.ts b/src/background/services/wallet/WalletService.test.ts index 26a521606..ccc6d3c54 100644 --- a/src/background/services/wallet/WalletService.test.ts +++ b/src/background/services/wallet/WalletService.test.ts @@ -17,7 +17,7 @@ import { BitcoinProvider, DerivationPath, getPublicKeyFromPrivateKey, - getAddressPublicKeyFromXPub, + getAddressDerivationPath, Avalanche, createWalletPolicy, LedgerSigner, @@ -40,7 +40,11 @@ import { UnsignedTx } from '@avalabs/avalanchejs'; import { FireblocksService } from '../fireblocks/FireblocksService'; import { SecretsService } from '../secrets/SecretsService'; import { Account, AccountType } from '../accounts/models'; -import { SecretType } from '../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../secrets/models'; import { Transaction } from 'bitcoinjs-lib'; import { SeedlessSessionManager } from '../seedless/SeedlessSessionManager'; import { Network } from '../network/models'; @@ -49,6 +53,9 @@ import { AccountsService } from '../accounts/AccountsService'; import { ed25519 } from '@noble/curves/ed25519'; import { HVMWallet } from './HVMWallet'; import { TransactionPayload, VMABI } from 'hypersdk-client'; +import { buildExtendedPublicKey } from '../secrets/utils'; +import { expectToThrowErroCode } from '@src/tests/test-utils'; +import { SecretsError } from '@src/utils/errors'; jest.mock('../network/NetworkService'); jest.mock('../secrets/SecretsService'); @@ -115,16 +122,21 @@ describe('background/services/wallet/WalletService.ts', () => { const WALLET_ID = 'wallet-id'; + const extendedPublicKeys = [ + buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey('xpubXP', AVALANCHE_BASE_DERIVATION_PATH), + ]; + const mockMnemonicWallet = ( - additionalData = {}, + additionalData: any = {}, account?: Partial, ) => { const data = { secretType: SecretType.Mnemonic, mnemonic: 'mnemonic', - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys, + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, walletId: WALLET_ID, name: 'mnemonic', ...additionalData, @@ -139,7 +151,7 @@ describe('background/services/wallet/WalletService.ts', () => { secretsService.getPrimaryWalletsDetails.mockResolvedValue([ { type: data.secretType, - derivationPath: data.derivationPath, + derivationPath: data.derivationPathSpec, id: data.walletId, name: data.name, }, @@ -149,16 +161,35 @@ describe('background/services/wallet/WalletService.ts', () => { }; const mockLedgerWallet = ( - additionalData = {}, + additionalData: any = {}, account?: Partial, ) => { const data = { secretType: SecretType.Ledger, - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, walletId: WALLET_ID, name: 'ledger', + publicKeys: [ + { + key: 'evm', + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.BIP44, + 'EVM', + ), + }, + { + key: 'xp', + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.BIP44, + 'AVM', + ), + }, + ], + extendedPublicKeys: [], ...additionalData, account: { type: AccountType.PRIMARY, @@ -171,7 +202,7 @@ describe('background/services/wallet/WalletService.ts', () => { secretsService.getPrimaryWalletsDetails.mockResolvedValue([ { type: data.secretType, - derivationPath: data.derivationPath, + derivationPath: data.derivationPathSpec, id: data.walletId, name: data.name, }, @@ -187,7 +218,26 @@ describe('background/services/wallet/WalletService.ts', () => { const data = { secretType: SecretType.LedgerLive, derivationPath: DerivationPath.LedgerLive, - pubKeys: [{ evm: 'evm', xp: 'xp' }], + publicKeys: [ + { + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.LedgerLive, + 'EVM', + ), + key: 'evm', + }, + { + key: 'xp', + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.LedgerLive, + 'AVM', + ), + }, + ], walletId: WALLET_ID, name: 'ledger live', ...additionalData, @@ -218,7 +268,7 @@ describe('background/services/wallet/WalletService.ts', () => { const data = { secretType: SecretType.Seedless, derivationPath: DerivationPath.BIP44, - pubKeys: [{ evm: 'evm', xp: 'xp' }], + publicKeys: [{ evm: 'evm', xp: 'xp' }], walletId: WALLET_ID, name: 'seedles', userId: '123', @@ -262,6 +312,16 @@ describe('background/services/wallet/WalletService.ts', () => { secretsService.getPrimaryWalletsDetails = jest.fn().mockResolvedValue([]); + jest + .mocked(getAddressDerivationPath) + .mockImplementation((index, pathSpec, vm) => { + const coin = vm === 'EVM' ? 60 : 9000; + + return pathSpec === DerivationPath.BIP44 + ? `m/44'/${coin}'/0'/0/${index}` + : `m/44'/${coin}'/${index}'/0/0`; + }); + getAddressMock = jest.fn().mockImplementation((_pubkey, chain) => { return `${chain}-`; }); @@ -385,9 +445,9 @@ describe('background/services/wallet/WalletService.ts', () => { await walletService.init({ mnemonic, - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys, + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, secretType: SecretType.Mnemonic, }); @@ -395,9 +455,9 @@ describe('background/services/wallet/WalletService.ts', () => { expect.objectContaining({ mnemonic, secretType: SecretType.Mnemonic, - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys, + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, }), ); expect(onUnlockSpy).toHaveBeenCalled(); @@ -456,9 +516,9 @@ describe('background/services/wallet/WalletService.ts', () => { it('should save the new wallet values', async () => { const params = { mnemonic: 'mnemonic', - xpub: 'xpub', - xpubXP: 'xpubXP', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys, + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, secretType: SecretType.Mnemonic, } as AddPrimaryWalletSecrets; (getDerivationPath as jest.Mock).mockReturnValueOnce( @@ -1302,65 +1362,82 @@ describe('background/services/wallet/WalletService.ts', () => { it('throws if pubKeys for the account are missing', async () => { mockLedgerLiveWallet({}, { index: 1 }); - await expect(walletService.getActiveAccountPublicKey()).rejects.toThrow( - 'Can not find public key for the given index', + await expectToThrowErroCode( + walletService.getActiveAccountPublicKey(), + SecretsError.PublicKeyNotFound, ); }); it('returns the public keys for mnemonic wallets', async () => { - const { xpub, xpubXP } = mockMnemonicWallet({}, { index: 0 }); - (ed25519.getPublicKey as jest.Mock).mockReturnValue('123123'); - jest - .mocked(getAddressPublicKeyFromXPub) - .mockReturnValueOnce(evmPub as any); - jest - .mocked(Avalanche.getAddressPublicKeyFromXpub) - .mockReturnValueOnce(xpPub as any); + const { publicKeys } = mockMnemonicWallet( + { + publicKeys: [ + { + curve: 'secp256k1', + key: 'evm', + derivationPath: `m/44'/60'/0'/0/0`, + }, + { + curve: 'secp256k1', + key: 'xp', + derivationPath: `m/44'/9000'/0'/0/0`, + }, + { + curve: 'ed25519', + key: 'hvm', + derivationPath: `m/44'/9000'/0'/0/0`, + }, + ], + }, + { index: 0 }, + ); const result = await walletService.getActiveAccountPublicKey(); expect(result).toStrictEqual({ - evm: evmPub, - xp: xpPub, - ed25519: '313233313233', + evm: publicKeys[0]!.key, + xp: publicKeys[1]!.key, + ed25519: publicKeys[2]!.key, }); - - expect(getAddressPublicKeyFromXPub).toHaveBeenCalledWith(xpub, 0); - expect(Avalanche.getAddressPublicKeyFromXpub).toHaveBeenCalledWith( - xpubXP, - 0, - ); }); it('returns the public keys based on the extended public key correctly', async () => { - const { xpub, xpubXP } = mockLedgerWallet({}, { index: 0 }); - - (getAddressPublicKeyFromXPub as jest.Mock).mockReturnValueOnce(evmPub); - ( - Avalanche.getAddressPublicKeyFromXpub as jest.Mock - ).mockReturnValueOnce(xpPub); + mockLedgerWallet( + { + extendedPublicKeys, + }, + { index: 0 }, + ); const result = await walletService.getActiveAccountPublicKey(); expect(result).toStrictEqual({ - evm: evmPub, - xp: xpPub, + evm: 'evm', + xp: 'xp', }); - - expect(getAddressPublicKeyFromXPub).toHaveBeenCalledWith(xpub, 0); - expect(Avalanche.getAddressPublicKeyFromXpub).toHaveBeenCalledWith( - xpubXP, - 0, - ); }); it('returns the public keys based from the storage', async () => { mockLedgerLiveWallet( { - pubKeys: [ + publicKeys: [ { - evm: evmPub, - xp: xpPub, + key: evmPub, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.LedgerLive, + 'EVM', + ), + }, + { + key: xpPub, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.LedgerLive, + 'AVM', + ), }, ], }, @@ -1378,10 +1455,24 @@ describe('background/services/wallet/WalletService.ts', () => { it('returns the public keys for seedless wallets', async () => { mockSeedlessWallet( { - pubKeys: [ + publicKeys: [ + { + key: evmPub, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.BIP44, + 'EVM', + ), + }, { - evm: evmPub, - xp: xpPub, + key: xpPub, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + 0, + DerivationPath.BIP44, + 'AVM', + ), }, ], }, @@ -1424,8 +1515,9 @@ describe('background/services/wallet/WalletService.ts', () => { }, ); - await expect(walletService.getActiveAccountPublicKey()).rejects.toThrow( - 'Unable to get public key', + await expectToThrowErroCode( + walletService.getActiveAccountPublicKey(), + SecretsError.PublicKeyNotFound, ); }); @@ -1586,7 +1678,10 @@ describe('background/services/wallet/WalletService.ts', () => { it('returns the correct list of addresses', async () => { secretsService.getPrimaryAccountSecrets.mockResolvedValueOnce({ - xpubXP, + extendedPublicKeys: [ + // buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey('xpubXP', AVALANCHE_BASE_DERIVATION_PATH), + ], } as any); (Avalanche.getAddressFromXpub as jest.Mock) diff --git a/src/background/services/wallet/WalletService.ts b/src/background/services/wallet/WalletService.ts index e15048dfe..3bdd3c2de 100644 --- a/src/background/services/wallet/WalletService.ts +++ b/src/background/services/wallet/WalletService.ts @@ -20,9 +20,7 @@ import { BitcoinProviderAbstract, BitcoinWallet, createWalletPolicy, - DerivationPath, getAddressDerivationPath, - getAddressPublicKeyFromXPub, getPublicKeyFromPrivateKey, getWalletFromMnemonic, JsonRpcBatchInternal, @@ -71,7 +69,14 @@ import { Account } from '../accounts/models'; import { HVMWallet } from './HVMWallet'; import { ed25519 } from '@noble/curves/ed25519'; import { strip0x } from '@avalabs/core-utils-sdk'; -import { getAccountPrivateKeyFromMnemonic } from '../secrets/utils/getAccountPrivateKeyFromMnemonic'; +import { + getExtendedPublicKeyFor, + getPublicKeyFor, + isPrimaryWalletSecrets, +} from '../secrets/utils'; +import { assertPresent } from '@src/utils/assertions'; +import { CommonError, LedgerError, SecretsError } from '@src/utils/errors'; +import { omitUndefined } from '@src/utils/object'; @singleton() export class WalletService implements OnUnlock { @@ -141,13 +146,16 @@ export class WalletService implements OnUnlock { 'Mnemonic or xpub or pubKey is required to create a new wallet!', ); } - if (secrets.secretType === SecretType.LedgerLive && !secrets.pubKeys) { + if ( + secrets.secretType === SecretType.LedgerLive && + !secrets.publicKeys?.length + ) { throw new Error('PubKey is required to create a new wallet!'); } if ( (secrets.secretType === SecretType.Keystone || secrets.secretType === SecretType.Ledger) && - !secrets.xpub + !secrets.extendedPublicKeys?.length ) { throw new Error( 'Mnemonic or xpub or pubKey is required to create a new wallet!', @@ -184,7 +192,7 @@ export class WalletService implements OnUnlock { return HVMWallet.fromMnemonic( secrets.mnemonic, accountIndexToUse, - secrets.derivationPath, + secrets.derivationPathSpec, ); } if (secretType === SecretType.PrivateKey) { @@ -201,7 +209,7 @@ export class WalletService implements OnUnlock { if (secretType === SecretType.Seedless) { const accountIndexToUse = accountIndex === undefined ? secrets.account.index : accountIndex; - const addressPublicKey = secrets.pubKeys[accountIndexToUse]; + const addressPublicKey = secrets.publicKeys[accountIndexToUse]; if (!addressPublicKey) { throw new Error('Account public key not available'); @@ -225,7 +233,7 @@ export class WalletService implements OnUnlock { const signer = getWalletFromMnemonic( secrets.mnemonic, accountIndexToUse, - secrets.derivationPath, + secrets.derivationPathSpec, ); return signer.connect(provider as JsonRpcBatchInternal); } @@ -243,7 +251,7 @@ export class WalletService implements OnUnlock { return new LedgerSigner( accountIndexToUse, this.ledgerService.recentTransport, - secrets.derivationPath, + secrets.derivationPathSpec, provider as JsonRpcBatchInternal, ); } @@ -288,15 +296,6 @@ export class WalletService implements OnUnlock { // Bitcoin signers if (network.vmName === NetworkVMType.BITCOIN) { - if (secretType === SecretType.Mnemonic) { - const accountIndexToUse = - accountIndex === undefined ? secrets.account.index : accountIndex; - return await BitcoinWallet.fromMnemonic( - secrets.mnemonic, - accountIndexToUse, - provider as BitcoinProviderAbstract, - ); - } if (secretType === SecretType.Fireblocks) { if (!secrets.api) { throw new Error(`Fireblocks API access keys not configured`); @@ -316,17 +315,37 @@ export class WalletService implements OnUnlock { ); } + if (!isPrimaryWalletSecrets(secrets)) { + throw new Error( + `No proper signer could be constructed for Bitcoin and ${secretType} account`, + ); + } + + const accountIndexToUse = + accountIndex === undefined ? secrets.account.index : accountIndex; + + if (secretType === SecretType.Mnemonic) { + return await BitcoinWallet.fromMnemonic( + secrets.mnemonic, + accountIndexToUse, + provider as BitcoinProviderAbstract, + ); + } + + const derivationPath = getAddressDerivationPath( + accountIndexToUse, + secrets.derivationPathSpec, + 'EVM', + ); + const publicKey = getPublicKeyFor(secrets, derivationPath, 'secp256k1'); + + assertPresent(publicKey, SecretsError.PublicKeyNotFound); + if (secretType === SecretType.Keystone) { - const accountIndexToUse = - accountIndex === undefined ? secrets.account.index : accountIndex; return new BitcoinKeystoneWallet( secrets.masterFingerprint, - getAddressPublicKeyFromXPub(secrets.xpub, accountIndexToUse), - getAddressDerivationPath( - accountIndexToUse, - secrets.derivationPath, - 'EVM', - ), + Buffer.from(publicKey.key, 'hex'), + derivationPath, this.keystoneService, provider as BitcoinProviderAbstract, tabId, @@ -341,16 +360,10 @@ export class WalletService implements OnUnlock { const walletPolicy = await this.parseWalletPolicyDetails( this.accountsService.activeAccount, ); - const accountIndexToUse = - accountIndex === undefined ? secrets.account.index : accountIndex; return new BitcoinLedgerWallet( - getAddressPublicKeyFromXPub(secrets.xpub, accountIndexToUse), - getAddressDerivationPath( - accountIndexToUse, - secrets.derivationPath, - 'EVM', - ), + Buffer.from(publicKey.key, 'hex'), + derivationPath, provider as BitcoinProviderAbstract, this.ledgerService.recentTransport, walletPolicy, @@ -362,35 +375,18 @@ export class WalletService implements OnUnlock { if (!this.ledgerService.recentTransport) { throw new Error('Ledger transport not available'); } - - const accountIndexToUse = - accountIndex === undefined ? secrets.account.index : accountIndex; - const addressPublicKey = secrets.pubKeys[accountIndexToUse]; - - if (!addressPublicKey) { - throw new Error('Account public key not available'); - } - const walletPolicy = await this.parseWalletPolicyDetails( secrets.account, ); return new BitcoinLedgerWallet( - Buffer.from(addressPublicKey.evm, 'hex'), - getAddressDerivationPath( - accountIndexToUse, - secrets.derivationPath, - 'EVM', - ), + Buffer.from(publicKey.key, 'hex'), + derivationPath, provider as BitcoinProviderAbstract, this.ledgerService.recentTransport, walletPolicy, ); } - - throw new Error( - `No proper signer could be constructed for Bitcoin and ${secretType} account`, - ); } // Avalanche signers @@ -405,51 +401,72 @@ export class WalletService implements OnUnlock { } if (secretType === SecretType.Ledger) { - if (!this.ledgerService.recentTransport) { - throw new Error('Ledger transport not available'); - } + assertPresent( + this.ledgerService.recentTransport, + LedgerError.TransportNotFound, + ); + const accountIndexToUse = accountIndex === undefined ? secrets.account.index : accountIndex; + const derivationPath = getAddressDerivationPath( + accountIndexToUse, + secrets.derivationPathSpec, + 'AVM', + ); + const extPublicKey = getExtendedPublicKeyFor( + secrets.extendedPublicKeys, + derivationPath, + 'secp256k1', + ); + + assertPresent(extPublicKey, SecretsError.MissingExtendedPublicKey); + return new Avalanche.SimpleLedgerSigner( accountIndexToUse, provider as Avalanche.JsonRpcProvider, - secrets.xpubXP, + extPublicKey.key, ); } if (secretType === SecretType.LedgerLive) { - if (!this.ledgerService.recentTransport) { - throw new Error('Ledger transport not available'); - } + assertPresent( + this.ledgerService.recentTransport, + LedgerError.TransportNotFound, + ); const accountIndexToUse = accountIndex === undefined ? secrets.account.index : accountIndex; - const pubkey = secrets.pubKeys[accountIndexToUse]; - - if (!pubkey) { - throw new Error('Cannot find public key for the active account'); - } + const derivationPathEVM = getAddressDerivationPath( + accountIndexToUse, + secrets.derivationPathSpec, + 'EVM', + ); + const derivationPathAVM = getAddressDerivationPath( + accountIndexToUse, + secrets.derivationPathSpec, + 'AVM', + ); + const pubkeyEVM = getPublicKeyFor( + secrets, + derivationPathEVM, + 'secp256k1', + ); + const pubkeyAVM = getPublicKeyFor( + secrets, + derivationPathAVM, + 'secp256k1', + ); - // Verify public key exists for X/P path - if (!pubkey.xp) { - throw new Error('X/P Chain public key is not set'); - } + assertPresent(pubkeyEVM, SecretsError.PublicKeyNotFound); + assertPresent(pubkeyAVM, SecretsError.PublicKeyNotFound); // TODO: SimpleLedgerSigner doesn't support LedgerLive derivation paths ATM // https://ava-labs.atlassian.net/browse/CP-5861 return new Avalanche.LedgerSigner( - Buffer.from(pubkey.xp, 'hex'), - getAddressDerivationPath( - accountIndexToUse, - DerivationPath.LedgerLive, - 'AVM', - ), - Buffer.from(pubkey.evm, 'hex'), - getAddressDerivationPath( - accountIndexToUse, - DerivationPath.LedgerLive, - 'EVM', - ), + Buffer.from(pubkeyAVM.key, 'hex'), + derivationPathAVM, + Buffer.from(pubkeyEVM.key, 'hex'), + derivationPathEVM, provider as Avalanche.JsonRpcProvider, ); } @@ -666,70 +683,34 @@ export class WalletService implements OnUnlock { }; } - if (secrets.secretType === SecretType.Mnemonic && secrets.account) { - const evmPub = getAddressPublicKeyFromXPub( - secrets.xpub, - secrets.account.index, - ); - - const ed25519Pub = Buffer.from( - ed25519.getPublicKey( - strip0x( - getAccountPrivateKeyFromMnemonic( - secrets.mnemonic, - secrets.account.index, - secrets.derivationPath, - ), - ), - ), - ); - - const xpPub = Avalanche.getAddressPublicKeyFromXpub( - secrets.xpubXP, - secrets.account.index, - ); + assertPresent(secrets.account, CommonError.NoActiveAccount); - return { - evm: evmPub.toString('hex'), - xp: xpPub.toString('hex'), - ed25519: ed25519Pub.toString('hex'), - }; - } - - if ( - secrets.secretType === SecretType.Ledger && - secrets.xpubXP && - secrets.account - ) { - const evmPub = getAddressPublicKeyFromXPub( - secrets.xpub, - secrets.account.index, - ); - const xpPub = Avalanche.getAddressPublicKeyFromXpub( - secrets.xpubXP, - secrets.account.index, - ); - - return { - evm: evmPub.toString('hex'), - xp: xpPub.toString('hex'), - }; - } - - if ( - (secrets.secretType === SecretType.LedgerLive || - secrets.secretType === SecretType.Seedless) && - secrets.account - ) { - const publicKey = secrets.pubKeys[secrets.account.index]; + const derivationPathEVM = getAddressDerivationPath( + secrets.account.index, + secrets.derivationPathSpec, + 'EVM', + ); + const derivationPathAVM = getAddressDerivationPath( + secrets.account.index, + secrets.derivationPathSpec, + 'AVM', + ); - if (!publicKey) - throw new Error('Can not find public key for the given index'); + const evmPub = getPublicKeyFor(secrets, derivationPathEVM, 'secp256k1'); + const avmPub = getPublicKeyFor(secrets, derivationPathAVM, 'secp256k1'); + const hvmPub = getPublicKeyFor(secrets, derivationPathAVM, 'ed25519'); - return publicKey; - } + assertPresent( + evmPub, + SecretsError.PublicKeyNotFound, + `EVM @ ${derivationPathEVM}`, + ); - throw new Error('Unable to get public key'); + return omitUndefined({ + evm: evmPub?.key, + xp: avmPub?.key, + ed25519: hvmPub?.key, + }); } /** @@ -887,7 +868,7 @@ export class WalletService implements OnUnlock { this.accountsService.activeAccount, ); - if (!secrets || !secrets.xpubXP) { + if (!secrets || !('extendedPublicKeys' in secrets)) { return []; } @@ -895,9 +876,28 @@ export class WalletService implements OnUnlock { return []; } + assertPresent(indices[0], SecretsError.NoAccountIndex); + + const avmDerivationPath = getAddressDerivationPath( + 0, + secrets.derivationPathSpec, + 'AVM', + ); + const avmExtendedPubKey = getExtendedPublicKeyFor( + secrets.extendedPublicKeys, + avmDerivationPath, + 'secp256k1', + ); + + assertPresent( + avmExtendedPubKey, + SecretsError.MissingExtendedPublicKey, + `AVM @ ${avmDerivationPath}`, + ); + return indices.map((index) => Avalanche.getAddressFromXpub( - secrets.xpubXP as string, + avmExtendedPubKey.key, index, provXP, chainAlias, diff --git a/src/background/services/wallet/handlers/importLedger.test.ts b/src/background/services/wallet/handlers/importLedger.test.ts index 51b1ae920..18e72a058 100644 --- a/src/background/services/wallet/handlers/importLedger.test.ts +++ b/src/background/services/wallet/handlers/importLedger.test.ts @@ -3,9 +3,18 @@ import { WalletService } from '../WalletService'; import { SecretsService } from '../../secrets/SecretsService'; import { AccountsService } from '../../accounts/AccountsService'; import { ImportLedgerHandler } from './importLedger'; -import { SecretType } from '../../secrets/models'; -import { DerivationPath } from '@avalabs/core-wallets-sdk'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; +import { + DerivationPath, + getAddressDerivationPath, +} from '@avalabs/core-wallets-sdk'; import { buildRpcCall } from '@src/tests/test-utils'; +import { buildExtendedPublicKey } from '../../secrets/utils'; +import { AddressPublicKey } from '../../secrets/AddressPublicKey'; describe('src/background/services/wallet/handlers/importLedger', () => { const walletService = { @@ -58,7 +67,7 @@ describe('src/background/services/wallet/handlers/importLedger', () => { expect(error).toEqual('Missing required param: Need xpub or pubKeys'); }); - it('returns an error if the seed phrase is already imported', async () => { + it('returns an error if the wallet is already imported', async () => { secretsService.isKnownSecret.mockResolvedValueOnce(true); const { error } = await handle({ @@ -94,9 +103,12 @@ describe('src/background/services/wallet/handlers/importLedger', () => { walletService.addPrimaryWallet.mockResolvedValue(walletId); secretsService.getWalletAccountsSecretsById.mockResolvedValue({ secretType: SecretType.Ledger, - xpub: xpubValue, - xpubXP: xpubXPValue, - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey(xpubValue, `m/44'/60'/0'/0/0`), + buildExtendedPublicKey(xpubXPValue, `m/44'/9000'/0'/0/0`), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, id: walletId, name: nameValue, }); @@ -111,9 +123,12 @@ describe('src/background/services/wallet/handlers/importLedger', () => { expect(walletService.addPrimaryWallet).toHaveBeenCalledWith({ secretType: SecretType.Ledger, - xpub: xpubValue, - xpubXP: xpubXPValue, - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey(xpubValue, EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey(xpubXPValue, AVALANCHE_BASE_DERIVATION_PATH), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, name: nameValue, }); @@ -134,14 +149,14 @@ describe('src/background/services/wallet/handlers/importLedger', () => { { evm: 'pubKeyEvm', }, - ]; + ] as any; const nameValue = 'walletName'; secretsService.isKnownSecret.mockResolvedValueOnce(false); walletService.addPrimaryWallet.mockResolvedValue(walletId); secretsService.getWalletAccountsSecretsById.mockResolvedValue({ secretType: SecretType.LedgerLive, - pubKeys: pubKeysValue, - derivationPath: DerivationPath.LedgerLive, + publicKeys: pubKeysValue, + derivationPathSpec: DerivationPath.LedgerLive, id: walletId, name: nameValue, }); @@ -155,8 +170,18 @@ describe('src/background/services/wallet/handlers/importLedger', () => { expect(walletService.addPrimaryWallet).toHaveBeenCalledWith({ secretType: SecretType.LedgerLive, - pubKeys: pubKeysValue, - derivationPath: DerivationPath.LedgerLive, + publicKeys: pubKeysValue.map((key, index) => + AddressPublicKey.fromJSON({ + key: key.evm, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'EVM', + ), + }).toJSON(), + ), + derivationPathSpec: DerivationPath.LedgerLive, name: nameValue, }); @@ -185,14 +210,14 @@ describe('src/background/services/wallet/handlers/importLedger', () => { { evm: 'pubKeyEvm4', }, - ]; + ] as any; const nameValue = 'walletName'; secretsService.isKnownSecret.mockResolvedValueOnce(false); walletService.addPrimaryWallet.mockResolvedValue(walletId); secretsService.getWalletAccountsSecretsById.mockResolvedValue({ secretType: SecretType.LedgerLive, - pubKeys: pubKeysValue, - derivationPath: DerivationPath.LedgerLive, + publicKeys: pubKeysValue, + derivationPathSpec: DerivationPath.LedgerLive, id: walletId, name: nameValue, }); @@ -206,8 +231,18 @@ describe('src/background/services/wallet/handlers/importLedger', () => { expect(walletService.addPrimaryWallet).toHaveBeenCalledWith({ secretType: SecretType.LedgerLive, - pubKeys: pubKeysValue, - derivationPath: DerivationPath.LedgerLive, + publicKeys: pubKeysValue.map((key, index) => + AddressPublicKey.fromJSON({ + key: key.evm, + curve: 'secp256k1', + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'EVM', + ), + }).toJSON(), + ), + derivationPathSpec: DerivationPath.LedgerLive, name: nameValue, }); diff --git a/src/background/services/wallet/handlers/importLedger.ts b/src/background/services/wallet/handlers/importLedger.ts index b39baa27b..850761808 100644 --- a/src/background/services/wallet/handlers/importLedger.ts +++ b/src/background/services/wallet/handlers/importLedger.ts @@ -1,15 +1,27 @@ import { injectable } from 'tsyringe'; -import { DerivationPath } from '@avalabs/core-wallets-sdk'; +import { + DerivationPath, + getAddressDerivationPath, +} from '@avalabs/core-wallets-sdk'; +import { assertPresent } from '@src/utils/assertions'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; import { ExtensionRequestHandler } from '@src/background/connections/models'; -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + AddressPublicKeyJson, + EVM_BASE_DERIVATION_PATH, + ExtendedPublicKey, + SecretType, +} from '../../secrets/models'; import { WalletService } from '../WalletService'; import { SecretsService } from '../../secrets/SecretsService'; import { AccountsService } from '../../accounts/AccountsService'; import { ImportLedgerWalletParams, ImportWalletResult } from './models'; +import { SecretsError } from '@src/utils/errors'; +import { buildExtendedPublicKey } from '../../secrets/utils'; type HandlerType = ExtensionRequestHandler< ExtensionRequest.WALLET_IMPORT_LEDGER, @@ -63,22 +75,39 @@ export class ImportLedgerHandler implements HandlerType { error: `Invalid type: ${secretType}`, }; } - const secret = secretType === SecretType.Ledger ? xpub : pubKeys; - if (!secret) { + if (!xpub && !pubKeys) { return { ...request, error: `Missing required param: Need xpub or pubKeys`, }; } - const isKnown = await this.secretsService.isKnownSecret(secretType, secret); + if (secretType === SecretType.Ledger) { + assertPresent(xpub, SecretsError.MissingExtendedPublicKey); - if (isKnown) { - return { - ...request, - error: `This wallet already exists`, - }; + const isKnown = await this.secretsService.isKnownSecret(secretType, xpub); + + if (isKnown) { + return { + ...request, + error: 'This wallet already exists', + }; + } + } else if (secretType === SecretType.LedgerLive) { + assertPresent(pubKeys?.[0], SecretsError.PublicKeyNotFound); + + const isKnown = await this.secretsService.isKnownSecret( + secretType, + pubKeys[0].evm, + ); + + if (isKnown) { + return { + ...request, + error: 'This wallet already exists', + }; + } } if (dryRun) { @@ -94,12 +123,20 @@ export class ImportLedgerHandler implements HandlerType { let id: string; if (secretType === SecretType.Ledger) { + const extendedPublicKeys: ExtendedPublicKey[] = [ + buildExtendedPublicKey(xpub, EVM_BASE_DERIVATION_PATH), + ]; + if (xpubXP) { + extendedPublicKeys.push( + buildExtendedPublicKey(xpubXP, AVALANCHE_BASE_DERIVATION_PATH), + ); + } id = await this.walletService.addPrimaryWallet({ secretType, - xpub, - xpubXP, + derivationPathSpec: DerivationPath.BIP44, + extendedPublicKeys, + publicKeys: [], // Those will be populated at the end of this function name, - derivationPath: DerivationPath.BIP44, }); } else { if (!pubKeys) { @@ -109,11 +146,39 @@ export class ImportLedgerHandler implements HandlerType { }; } + const publicKeys: AddressPublicKeyJson[] = []; + + for (const [index, pubKey] of pubKeys.entries()) { + publicKeys.push({ + curve: 'secp256k1', + key: pubKey.evm, + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'EVM', + ), + type: 'address-pubkey', + }); + + if (pubKey.xp) { + publicKeys.push({ + curve: 'secp256k1', + key: pubKey.xp, + derivationPath: getAddressDerivationPath( + index, + DerivationPath.LedgerLive, + 'AVM', + ), + type: 'address-pubkey', + }); + } + } + id = await this.walletService.addPrimaryWallet({ secretType, - pubKeys, + derivationPathSpec: DerivationPath.LedgerLive, + publicKeys, name, - derivationPath: DerivationPath.LedgerLive, }); } diff --git a/src/background/services/wallet/handlers/importSeedPhrase.test.ts b/src/background/services/wallet/handlers/importSeedPhrase.test.ts index 212a93a64..e4a9d4e80 100644 --- a/src/background/services/wallet/handlers/importSeedPhrase.test.ts +++ b/src/background/services/wallet/handlers/importSeedPhrase.test.ts @@ -6,9 +6,14 @@ import { AccountsService } from '../../accounts/AccountsService'; import { ImportSeedPhraseHandler } from './importSeedPhrase'; import { SeedphraseImportError } from './models'; -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; import { DerivationPath } from '@avalabs/core-wallets-sdk'; import { buildRpcCall } from '@src/tests/test-utils'; +import { buildExtendedPublicKey } from '../../secrets/utils'; describe('src/background/services/wallet/handlers/importSeedPhrase', () => { const walletService = { @@ -81,10 +86,18 @@ describe('src/background/services/wallet/handlers/importSeedPhrase', () => { const expectedCall = { secretType: SecretType.Mnemonic, mnemonic: mnemonicLowerCase, - xpub: 'xpub6DPsyHV2MhmxaY7FtPeVVeB1MCZ9XzDhHTHFqzq2BVMKnqCcHjnXCeTZWUbsarGTdWHHz7wFdNfKiggZYabqj3b8FodX7cDryEQgBWqPcY6', - xpubXP: - 'xpub6CiZCQeZSo8jyK2ARkRKkFvo6rkaA9deyRaKNW2nFsbb8C3cnVLZxYuQ8YRABbBUA47xYd1EHoTqWtFX895Pb2VjcJUFD4kGbmetuh57mry', - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey( + 'xpub6DPsyHV2MhmxaY7FtPeVVeB1MCZ9XzDhHTHFqzq2BVMKnqCcHjnXCeTZWUbsarGTdWHHz7wFdNfKiggZYabqj3b8FodX7cDryEQgBWqPcY6', + EVM_BASE_DERIVATION_PATH, + ), + buildExtendedPublicKey( + 'xpub6CiZCQeZSo8jyK2ARkRKkFvo6rkaA9deyRaKNW2nFsbb8C3cnVLZxYuQ8YRABbBUA47xYd1EHoTqWtFX895Pb2VjcJUFD4kGbmetuh57mry', + AVALANCHE_BASE_DERIVATION_PATH, + ), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, name: 'Dummy mnemonic', }; @@ -118,10 +131,18 @@ describe('src/background/services/wallet/handlers/importSeedPhrase', () => { expect(walletService.addPrimaryWallet).toHaveBeenCalledWith({ secretType: SecretType.Mnemonic, mnemonic, - xpub: 'xpub6DPsyHV2MhmxaY7FtPeVVeB1MCZ9XzDhHTHFqzq2BVMKnqCcHjnXCeTZWUbsarGTdWHHz7wFdNfKiggZYabqj3b8FodX7cDryEQgBWqPcY6', - xpubXP: - 'xpub6CiZCQeZSo8jyK2ARkRKkFvo6rkaA9deyRaKNW2nFsbb8C3cnVLZxYuQ8YRABbBUA47xYd1EHoTqWtFX895Pb2VjcJUFD4kGbmetuh57mry', - derivationPath: DerivationPath.BIP44, + publicKeys: [], + extendedPublicKeys: [ + buildExtendedPublicKey( + 'xpub6DPsyHV2MhmxaY7FtPeVVeB1MCZ9XzDhHTHFqzq2BVMKnqCcHjnXCeTZWUbsarGTdWHHz7wFdNfKiggZYabqj3b8FodX7cDryEQgBWqPcY6', + EVM_BASE_DERIVATION_PATH, + ), + buildExtendedPublicKey( + 'xpub6CiZCQeZSo8jyK2ARkRKkFvo6rkaA9deyRaKNW2nFsbb8C3cnVLZxYuQ8YRABbBUA47xYd1EHoTqWtFX895Pb2VjcJUFD4kGbmetuh57mry', + AVALANCHE_BASE_DERIVATION_PATH, + ), + ], + derivationPathSpec: DerivationPath.BIP44, name: 'Dummy mnemonic', }); diff --git a/src/background/services/wallet/handlers/importSeedPhrase.ts b/src/background/services/wallet/handlers/importSeedPhrase.ts index ad34416fc..7b8b952f9 100644 --- a/src/background/services/wallet/handlers/importSeedPhrase.ts +++ b/src/background/services/wallet/handlers/importSeedPhrase.ts @@ -9,7 +9,11 @@ import { ethErrors } from 'eth-rpc-errors'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; import { ExtensionRequestHandler } from '@src/background/connections/models'; -import { SecretType } from '../../secrets/models'; +import { + AVALANCHE_BASE_DERIVATION_PATH, + EVM_BASE_DERIVATION_PATH, + SecretType, +} from '../../secrets/models'; import { WalletService } from '../WalletService'; import { SecretsService } from '../../secrets/SecretsService'; import { AccountsService } from '../../accounts/AccountsService'; @@ -19,6 +23,7 @@ import { ImportWalletResult, SeedphraseImportError, } from './models'; +import { buildExtendedPublicKey } from '../../secrets/utils'; type HandlerType = ExtensionRequestHandler< ExtensionRequest.WALLET_IMPORT_SEED_PHRASE, @@ -79,9 +84,12 @@ export class ImportSeedPhraseHandler implements HandlerType { const id = await this.walletService.addPrimaryWallet({ secretType: SecretType.Mnemonic, mnemonic, - xpub, - xpubXP, - derivationPath: DerivationPath.BIP44, + extendedPublicKeys: [ + buildExtendedPublicKey(xpub, EVM_BASE_DERIVATION_PATH), + buildExtendedPublicKey(xpubXP, AVALANCHE_BASE_DERIVATION_PATH), + ], + publicKeys: [], + derivationPathSpec: DerivationPath.BIP44, name, }); diff --git a/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.test.ts b/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.test.ts index 4b7e5c4c7..4f798254c 100644 --- a/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.test.ts +++ b/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.test.ts @@ -95,7 +95,7 @@ describe('src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.ts it('does nothing if the device is incorrect (BTC addresses dont match)', async () => { secretsServiceMock.getAccountSecrets.mockResolvedValue({ secretType: SecretType.Ledger, - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, account: { type: AccountType.PRIMARY, index: 0, @@ -128,7 +128,7 @@ describe('src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.ts it('stores the details if the device is correct for BIP44', async () => { secretsServiceMock.getAccountSecrets.mockResolvedValue({ secretType: SecretType.Ledger, - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, id: 'wallet-id', account: { type: AccountType.PRIMARY, @@ -172,7 +172,7 @@ describe('src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.ts it('stores the details if the device is correct for Ledger Live', async () => { secretsServiceMock.getAccountSecrets.mockResolvedValue({ secretType: SecretType.Ledger, - derivationPath: DerivationPath.BIP44, + derivationPathSpec: DerivationPath.BIP44, id: 'wallet-id', account: { type: AccountType.PRIMARY, diff --git a/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.ts b/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.ts index 8df23fe42..6cfbadbac 100644 --- a/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.ts +++ b/src/background/services/wallet/handlers/storeBtcWalletPolicyDetails.ts @@ -55,12 +55,14 @@ export class StoreBtcWalletPolicyDetails implements HandlerType { const [xpub, masterFingerPrint, hmacHex, name] = request.params; const isMainnet = this.networkService.isMainnet(); - if (!secrets.derivationPath) { + if (!secrets.derivationPathSpec) { throw new Error('unknown derivation path'); } const accountIndex = - secrets.derivationPath === DerivationPath.BIP44 ? activeAccount.index : 0; + secrets.derivationPathSpec === DerivationPath.BIP44 + ? activeAccount.index + : 0; const derivedAddressBtc = getBech32AddressFromXPub( xpub, diff --git a/src/background/vmModules/ApprovalController.test.ts b/src/background/vmModules/ApprovalController.test.ts index 79170d213..e6e0bad51 100644 --- a/src/background/vmModules/ApprovalController.test.ts +++ b/src/background/vmModules/ApprovalController.test.ts @@ -22,6 +22,7 @@ import { ActionType, MultiTxAction, } from '../services/actions/models'; +import { SecretsService } from '../services/secrets/SecretsService'; jest.mock('tsyringe', () => { return { @@ -198,6 +199,7 @@ describe('src/background/vmModules/ApprovalController', () => { let walletService: jest.Mocked; let networkService: jest.Mocked; + let secretsService: jest.Mocked; let controller: ApprovalController; beforeEach(() => { @@ -210,7 +212,15 @@ describe('src/background/vmModules/ApprovalController', () => { getNetwork: jest.fn(), } as any; - controller = new ApprovalController(walletService, networkService); + secretsService = { + derivePublicKey: jest.fn(), + } as any; + + controller = new ApprovalController( + secretsService, + walletService, + networkService, + ); jest.mocked(networkService.getNetwork).mockResolvedValue(btcNetwork); jest.mocked(getProviderForNetwork).mockReturnValue(provider); @@ -220,6 +230,22 @@ describe('src/background/vmModules/ApprovalController', () => { })); }); + describe('requestPublicKey()', () => { + it('passes the request down to the SecretsService', async () => { + controller.requestPublicKey({ + curve: 'ed25519', + derivationPath: 'm/44/1234/0/0', + secretId: 'secretId', + }); + + expect(secretsService.derivePublicKey).toHaveBeenCalledWith( + 'secretId', + 'ed25519', + 'm/44/1234/0/0', + ); + }); + }); + describe('updateTx()', () => { it('uses `updateTx` callback to update the transaction payload', async () => { const updateTx = jest.fn().mockImplementation(({ maxFeeRate }) => ({ diff --git a/src/background/vmModules/ApprovalController.ts b/src/background/vmModules/ApprovalController.ts index 15344ecca..a3f589f9f 100644 --- a/src/background/vmModules/ApprovalController.ts +++ b/src/background/vmModules/ApprovalController.ts @@ -13,6 +13,7 @@ import { EvmTxBatchUpdateFn, SigningRequest, SigningData_EthSendTx, + RequestPublicKeyParams, } from '@avalabs/vm-module-types'; import { rpcErrors, providerErrors } from '@metamask/rpc-errors'; @@ -33,6 +34,7 @@ import { MultiApprovalParamsWithContext, } from './models'; import { ACTION_HANDLED_BY_MODULE } from '../models'; +import { SecretsService } from '../services/secrets/SecretsService'; type CachedRequest = { params: ApprovalParams; @@ -54,10 +56,16 @@ type ActionToRequest = { export class ApprovalController implements BatchApprovalController { #walletService: WalletService; #networkService: NetworkService; + #secretsService: SecretsService; #requests = new Map(); - constructor(walletService: WalletService, networkService: NetworkService) { + constructor( + secretsService: SecretsService, + walletService: WalletService, + networkService: NetworkService, + ) { + this.#secretsService = secretsService; this.#walletService = walletService; this.#networkService = networkService; } @@ -70,6 +78,19 @@ export class ApprovalController implements BatchApprovalController { // Transaction Reverted. Show a toast? Trigger browser notification?', }; + async requestPublicKey({ + curve, + derivationPath, + secretId, + }: RequestPublicKeyParams): Promise { + // TODO: Ask user approval if needed? + return this.#secretsService.derivePublicKey( + secretId, + curve, + derivationPath, + ); + } + onRejected = async (action: A) => { if (!action.actionId) { return; diff --git a/src/background/vmModules/ModuleManager.ts b/src/background/vmModules/ModuleManager.ts index 8776720f5..bf3151151 100644 --- a/src/background/vmModules/ModuleManager.ts +++ b/src/background/vmModules/ModuleManager.ts @@ -73,7 +73,6 @@ export class ModuleManager { approvalController: this.#approvalController, appInfo, }), - new AvalancheModule({ environment, approvalController: this.#approvalController, diff --git a/src/hooks/useErrorMessage.ts b/src/hooks/useErrorMessage.ts index 11b56fd8a..f41212b16 100644 --- a/src/hooks/useErrorMessage.ts +++ b/src/hooks/useErrorMessage.ts @@ -3,7 +3,12 @@ import { useTranslation } from 'react-i18next'; import { errorCodes } from 'eth-rpc-errors'; import { FireblocksErrorCode } from '@src/background/services/fireblocks/models'; -import { CommonError, RpcErrorCode, isWrappedError } from '@src/utils/errors'; +import { + CommonError, + RpcErrorCode, + SecretsError, + isWrappedError, +} from '@src/utils/errors'; import { UnifiedBridgeError } from '@src/background/services/unifiedBridge/models'; import { KeystoreError } from '@src/utils/keystore/models'; import { SeedphraseImportError } from '@src/background/services/wallet/handlers/models'; @@ -142,6 +147,36 @@ export const useErrorMessage = () => { title: t('Request timed out'), hint: t('This is taking longer than expected. Please try again later.'), }, + [CommonError.ModuleManagerNotSet]: { + title: t('Internal error occurred.'), // Do not leak implementation details to the UI + }, + [CommonError.MigrationFailed]: { + title: t('Storage update failed'), + }, + }), + [t], + ); + + const secretErrors: Record = useMemo( + () => ({ + [SecretsError.MissingExtendedPublicKey]: { + title: t('Extended public key not found'), + }, + [SecretsError.NoAccountIndex]: { + title: t('No account index was provided'), + }, + [SecretsError.PublicKeyNotFound]: { + title: t('Public key not found'), + }, + [SecretsError.SecretsNotFound]: { + title: t('Wallet secrets not found for the requested ID'), + }, + [SecretsError.WalletAlreadyExists]: { + title: t('This wallet is already imported'), + }, + [SecretsError.DerivationPathMissing]: { + title: t('Attempted to use an unknown derivation path'), + }, }), [t], ); @@ -197,6 +232,7 @@ export const useErrorMessage = () => { ...keystoreErrors, ...seedphraseImportError, ...rpcErrors, + ...secretErrors, }), [ fireblocksErrors, @@ -206,6 +242,7 @@ export const useErrorMessage = () => { keystoreErrors, seedphraseImportError, rpcErrors, + secretErrors, ], ); diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index bddca1a5b..e69997063 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -98,6 +98,7 @@ "At least one address required": "At least one address required", "Atomic Memory Locked": "Atomic Memory Locked", "Atomic Memory Unlocked": "Atomic Memory Unlocked", + "Attempted to use an unknown derivation path": "Attempted to use an unknown derivation path", "Australian Dollar": "Australian Dollar", "Authenticator": "Authenticator", "Authenticator App": "Authenticator App", @@ -337,6 +338,7 @@ "Export Cancelled": "Export Cancelled", "Export Recovery Phrase": "Export Recovery Phrase", "Expose addresses": "Expose addresses", + "Extended public key not found": "Extended public key not found", "External addresses": "External addresses", "FAQ": "FAQ", "FIDO Device": "FIDO Device", @@ -442,6 +444,7 @@ "Insurance Buyer": "Insurance Buyer", "Internal addresses": "Internal addresses", "Internal error": "Internal error", + "Internal error occurred.": "Internal error occurred.", "Internal error. Please try again": "Internal error. Please try again", "Invalid Chain": "Invalid Chain", "Invalid Password": "Invalid Password", @@ -561,6 +564,7 @@ "No Recent Recipients": "No Recent Recipients", "No Thanks": "No Thanks", "No Transactions Found": "No Transactions Found", + "No account index was provided": "No account index was provided", "No account is active": "No account is active", "No active network": "No active network", "No assets": "No assets", @@ -668,6 +672,7 @@ "Provide valid numerical value for maximum fee": "Provide valid numerical value for maximum fee", "Provided recovery phrase is not valid.": "Provided recovery phrase is not valid.", "Public Key": "Public Key", + "Public key not found": "Public key not found", "QR Code": "QR Code", "Quotes are refreshed to reflect current market prices": "Quotes are refreshed to reflect current market prices", "RPC Headers Updated": "RPC Headers Updated", @@ -820,6 +825,7 @@ "Start Date": "Start Date", "Status": "Status", "Stay updated on latest airdrops, events and more! You can unsubscribe anytime. For more details, see our Privacy Policy": "Stay updated on latest airdrops, events and more! You can unsubscribe anytime. For more details, see our Privacy Policy", + "Storage update failed": "Storage update failed", "Strength: medium. This will do. ": "Strength: medium. This will do. ", "Strength: strong. Keep this one!": "Strength: strong. Keep this one!", "Strength: weak. Keep adding characters.": "Strength: weak. Keep adding characters.", @@ -898,6 +904,7 @@ "This transaction requires two approvals": "This transaction requires two approvals", "This wallet already exists": "This wallet already exists", "This wallet cannot be renamed": "This wallet cannot be renamed", + "This wallet is already imported": "This wallet is already imported", "This website": "This website", "Threshold": "Threshold", "Time Elapsed": "Time Elapsed", @@ -1026,6 +1033,7 @@ "Wallet Details": "Wallet Details", "Wallet Name": "Wallet Name", "Wallet renamed": "Wallet renamed", + "Wallet secrets not found for the requested ID": "Wallet secrets not found for the requested ID", "WalletConnect": "WalletConnect", "Warning": "Warning", "Warning: Verify Message Content": "Warning: Verify Message Content", diff --git a/src/pages/ApproveAction/TxBatchApprovalScreen.tsx b/src/pages/ApproveAction/TxBatchApprovalScreen.tsx index cc6f353c9..bddf088fa 100644 --- a/src/pages/ApproveAction/TxBatchApprovalScreen.tsx +++ b/src/pages/ApproveAction/TxBatchApprovalScreen.tsx @@ -34,7 +34,7 @@ import { useFeeCustomizer } from './hooks/useFeeCustomizer'; import { TransactionDetailsCardContent } from './components/TransactionDetailsCardContent'; import { DetailedCardWrapper } from './components/DetailedCardWrapper'; import { FlexScrollbars } from '@src/components/common/FlexScrollbars'; -import { hasDefined } from '@src/background/models'; +import { hasDefined } from '@src/utils/object'; export function TxBatchApprovalScreen() { const { t } = useTranslation(); diff --git a/src/tests/test-utils.tsx b/src/tests/test-utils.tsx index 326deee79..71de1b96d 100644 --- a/src/tests/test-utils.tsx +++ b/src/tests/test-utils.tsx @@ -5,6 +5,8 @@ import { JsonRpcRequestPayload, } from '@src/background/connections/dAppConnection/models'; import { PartialBy } from '@src/background/models'; +import { CommonError, ErrorCode } from '@src/utils/errors'; +import { ethErrors } from 'eth-rpc-errors'; const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => { // K2 ThemeProvider causes issues here. @@ -38,6 +40,21 @@ export const buildRpcCall = ( request: payload, }) as const; +export const expectToThrowErroCode = async ( + fnOrPromise: Function | Promise, // eslint-disable-line + reason: ErrorCode = CommonError.Unknown, +) => { + await expect( + typeof fnOrPromise === 'function' ? fnOrPromise() : fnOrPromise, + ).rejects.toThrow( + ethErrors.rpc.internal({ + data: matchingPayload({ + reason, + }), + }), + ); +}; + export * from '@testing-library/react'; export { customRender as render }; diff --git a/src/utils/assertions.ts b/src/utils/assertions.ts index eaad9d5ab..39fcb06da 100644 --- a/src/utils/assertions.ts +++ b/src/utils/assertions.ts @@ -4,16 +4,29 @@ import { CommonError, ErrorCode } from './errors'; export function assertPresent( value: T, reason: ErrorCode, + additionalInfo?: string, ): asserts value is NonNullable { - if (typeof value === 'undefined' || value === null) { + const isNullish = typeof value === 'undefined' || value === null; + const isEmptyBuffer = Buffer.isBuffer(value) && value.length === 0; + + if (isNullish || isEmptyBuffer || value === '') { throw ethErrors.rpc.internal({ data: { reason: reason ?? CommonError.Unknown, + additionalInfo, }, }); } } +export function assertPropDefined( + obj: T, + prop: K, + reason: ErrorCode, +): asserts obj is T & Record> { + assertPresent(obj[prop], reason); +} + type NonEmptyString = T extends '' ? never : T; export function assertNonEmptyString( diff --git a/src/utils/errors/errorCodes.ts b/src/utils/errors/errorCodes.ts index 1ad48b22a..a29742230 100644 --- a/src/utils/errors/errorCodes.ts +++ b/src/utils/errors/errorCodes.ts @@ -14,6 +14,28 @@ export enum CommonError { UnknownNetwork = 'unknown-network', UnknownNetworkFee = 'unknown-network-fee', RequestTimeout = 'request-timeout', + MigrationFailed = 'migration-failed', + ModuleManagerNotSet = 'module-manager-not-set', +} + +export enum LedgerError { + TransportNotFound = 'ledger-transport-not-found', + NoPublicKeyReturned = 'ledger-no-public-key-returned', +} + +export enum SecretsError { + SecretsNotFound = 'secrets-not-found', + MissingExtendedPublicKey = 'missing-ext-pubkey', + WalletAlreadyExists = 'wallet-already-exists', + PublicKeyNotFound = 'public-key-not-found', + NoAccountIndex = 'no-account-index', + DerivationPathMissing = 'derivation-path-missing', +} + +export enum AccountError { + EVMAddressNotFound = 'evm-address-not-found', + BTCAddressNotFound = 'btc-address-not-found', + NoAddressesFound = 'no-addresses-found', } export enum RpcErrorCode { @@ -27,4 +49,7 @@ export type ErrorCode = | SeedphraseImportError | KeystoreError | SeedlessError - | VMModuleError; + | VMModuleError + | SecretsError + | AccountError + | LedgerError; diff --git a/src/utils/object.ts b/src/utils/object.ts new file mode 100644 index 000000000..cbaa55d8c --- /dev/null +++ b/src/utils/object.ts @@ -0,0 +1,13 @@ +import { EnsureDefined, ExcludeUndefined } from '@src/background/models'; + +export const omitUndefined = >(obj: T) => + Object.fromEntries( + Object.entries(obj).filter(([, value]) => value !== undefined), + ) as ExcludeUndefined; + +export const hasDefined = ( + obj: T, + key: K, +): obj is EnsureDefined => { + return obj[key] !== undefined; +}; diff --git a/yarn.lock b/yarn.lock index d19c81355..9147d307e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,10 +29,10 @@ resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08" integrity sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA== -"@avalabs/avalanche-module@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@avalabs/avalanche-module/-/avalanche-module-1.2.1.tgz#e2282bfb3f3c241e29bf913f24661f5cbe74eefb" - integrity sha512-B7tOqWwFoCvxXWZyqm/Euc4k4d5INMFCoBBPtmPBBC5xos94byl/xNxfW/xjwrbbzO3mDK41skozAXPEMfacjw== +"@avalabs/avalanche-module@0.0.0-feat-solana-address-resolution-20250205161605": + version "0.0.0-feat-solana-address-resolution-20250205161605" + resolved "https://registry.yarnpkg.com/@avalabs/avalanche-module/-/avalanche-module-0.0.0-feat-solana-address-resolution-20250205161605.tgz#8e1b6147add594523a3c6b6c5e31e22b3eaa5a9c" + integrity sha512-RdToAxv2mk8DWwfCoeGrrWwvytIz8Mu95/xV2O6HLp+pOsOpu45BzLViffFCSAHD2bbtetIhbOEcUTmV1KCeOA== dependencies: "@avalabs/avalanchejs" "4.1.2-alpha.3" "@avalabs/core-chains-sdk" "3.1.0-alpha.32" @@ -42,7 +42,7 @@ "@avalabs/core-wallets-sdk" "3.1.0-alpha.32" "@avalabs/glacier-sdk" "3.1.0-alpha.32" "@avalabs/types" "3.1.0-alpha.32" - "@avalabs/vm-module-types" "1.2.1" + "@avalabs/vm-module-types" "0.0.0-feat-solana-address-resolution-20250205161605" "@metamask/rpc-errors" "6.3.0" big.js "6.2.1" bn.js "5.2.1" @@ -59,15 +59,15 @@ "@scure/base" "1.1.5" micro-eth-signer "0.7.2" -"@avalabs/bitcoin-module@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@avalabs/bitcoin-module/-/bitcoin-module-1.2.1.tgz#39e9fac9922f7ebd8d587c90cb133f49523518db" - integrity sha512-Ha2JXT2oJiUNiwutOtG7MlZZUp9nLVTYRjFAU1RFLi4EV2dj2qLmMfQqnNP4u1SmoEc6upXq8MerXnA/RrkGeA== +"@avalabs/bitcoin-module@0.0.0-feat-solana-address-resolution-20250205161605": + version "0.0.0-feat-solana-address-resolution-20250205161605" + resolved "https://registry.yarnpkg.com/@avalabs/bitcoin-module/-/bitcoin-module-0.0.0-feat-solana-address-resolution-20250205161605.tgz#f7a152c72e0ba2293bc345a7c2e4118bd0e51cdc" + integrity sha512-CulSUNpdx8CUAhOQu23YRNgW8T9t7lMiefVUjNoQUTSM+Qx9YMhnLl4SA975CHQR3/p77KNqEp9R1pWCaQYoyg== dependencies: "@avalabs/core-coingecko-sdk" "3.1.0-alpha.32" "@avalabs/core-utils-sdk" "3.1.0-alpha.32" "@avalabs/core-wallets-sdk" "3.1.0-alpha.32" - "@avalabs/vm-module-types" "1.2.1" + "@avalabs/vm-module-types" "0.0.0-feat-solana-address-resolution-20250205161605" "@metamask/rpc-errors" "6.3.0" big.js "6.2.1" bitcoinjs-lib "5.2.0" @@ -205,10 +205,10 @@ ledger-bitcoin "0.2.3" xss "1.0.14" -"@avalabs/evm-module@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@avalabs/evm-module/-/evm-module-1.2.1.tgz#06191095644da28f92f61cbf861cb2fcd42994f3" - integrity sha512-dA6x5vS86ygc4azOsmjFQESsj7+Sevc22HeiEjrxXJIJiJWPTLkNtyqSc92IQqBCHAX+Hf1cSDqAzo8alT0P/g== +"@avalabs/evm-module@0.0.0-feat-solana-address-resolution-20250205161605": + version "0.0.0-feat-solana-address-resolution-20250205161605" + resolved "https://registry.yarnpkg.com/@avalabs/evm-module/-/evm-module-0.0.0-feat-solana-address-resolution-20250205161605.tgz#903ece8af520507f2eca938f33aa240230c28051" + integrity sha512-xQX8jTimfsL8hD/KZ6/KhIqSndYdXronIv1Kdxts826o+YOeE49u84aqQFAHfaDjSjB3baEQddHcAX3ZaxHytw== dependencies: "@avalabs/core-coingecko-sdk" "3.1.0-alpha.32" "@avalabs/core-etherscan-sdk" "3.1.0-alpha.32" @@ -216,7 +216,7 @@ "@avalabs/core-wallets-sdk" "3.1.0-alpha.32" "@avalabs/glacier-sdk" "3.1.0-alpha.32" "@avalabs/types" "3.1.0-alpha.32" - "@avalabs/vm-module-types" "1.2.1" + "@avalabs/vm-module-types" "0.0.0-feat-solana-address-resolution-20250205161605" "@blockaid/client" "0.11.0" "@metamask/rpc-errors" "6.3.0" "@openzeppelin/contracts" "4.9.6" @@ -230,15 +230,16 @@ resolved "https://registry.yarnpkg.com/@avalabs/glacier-sdk/-/glacier-sdk-3.1.0-alpha.32.tgz#a03a130ed85f60dde076cf9feeb2a4c487aee8e4" integrity sha512-LTRdqpsiGIJVJxfcnNs/85/NnSneD3Jg+DZw6+F8AiJ1OcZftGu9Hz1GvWZhoz+9udHSbEXHEN5trws4vqTjkQ== -"@avalabs/hvm-module@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@avalabs/hvm-module/-/hvm-module-1.2.1.tgz#236f0c794ebbc1bc499f79731c9e26c0d037e86d" - integrity sha512-kBqXG46faGqRlM3BGqYkzLwJ4afPKu7qDuWLJHajVUX7fiGvfiy7APQga7NA5MaObE7fVfBWgq1cuV9v4+aaLQ== +"@avalabs/hvm-module@0.0.0-feat-solana-address-resolution-20250205161605": + version "0.0.0-feat-solana-address-resolution-20250205161605" + resolved "https://registry.yarnpkg.com/@avalabs/hvm-module/-/hvm-module-0.0.0-feat-solana-address-resolution-20250205161605.tgz#ba21bd0fca06bbe507a5a13f432ba462759822df" + integrity sha512-Gneid6eYDSMUOle2odCO3a4NLYZggTsQLnD8j9HOefdo14uIcIWvcBI28whV18AZ2TAlag0UUep19Z0kXE67Mw== dependencies: "@avalabs/core-utils-sdk" "3.1.0-alpha.30" - "@avalabs/vm-module-types" "1.2.1" + "@avalabs/vm-module-types" "0.0.0-feat-solana-address-resolution-20250205161605" "@metamask/rpc-errors" "6.3.0" "@noble/hashes" "1.5.0" + "@scure/base" "1.2.4" hypersdk-client "0.4.16" zod "3.23.8" @@ -258,10 +259,10 @@ resolved "https://registry.yarnpkg.com/@avalabs/types/-/types-3.1.0-alpha.32.tgz#bb94019e3c3121bfc66e1d5fed5a8c5c78c10fb5" integrity sha512-EwIMmTPygrMMxA8GVfBL7UNia3IycMjkPxuaRuZ8PTkW2Yr24F7GGeufBMFU69N0xTyoM8VQgMYNZ2x7stpedg== -"@avalabs/vm-module-types@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@avalabs/vm-module-types/-/vm-module-types-1.2.1.tgz#4d26e81dd228fd4d1aa9f3f3e434417fc08c531e" - integrity sha512-XR26j9oSQX/4EfdeJ9Nk/jl8QmaSXb84owAEeEjr2h5SJ/fNxHxE0l2om1TwYSHKO8zWDjAY9HliKJj25ou+0A== +"@avalabs/vm-module-types@0.0.0-feat-solana-address-resolution-20250205161605": + version "0.0.0-feat-solana-address-resolution-20250205161605" + resolved "https://registry.yarnpkg.com/@avalabs/vm-module-types/-/vm-module-types-0.0.0-feat-solana-address-resolution-20250205161605.tgz#3f712f76a6848bca75687d5f7a4d3c0bd9740720" + integrity sha512-OFAJVuuPya4+AblWIiniQIxekFDBCC4tAiw3jFmWPFDX6RSSvMXI+Z9+ex2WWKlCM42wAGoZi9DrPYtsthIFqw== dependencies: "@avalabs/core-wallets-sdk" "3.1.0-alpha.32" "@avalabs/glacier-sdk" "3.1.0-alpha.32" @@ -2223,9 +2224,9 @@ regenerator-runtime "^0.13.4" "@babel/runtime@^7.19.4", "@babel/runtime@^7.23.2": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" - integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341" + integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ== dependencies: regenerator-runtime "^0.14.0" @@ -4617,6 +4618,11 @@ dependencies: eslint-scope "5.1.1" +"@noble/ciphers@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.2.1.tgz#3812b72c057a28b44ff0ad4aff5ca846e5b9cdc9" + integrity sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA== + "@noble/curves@1.2.0", "@noble/curves@^1.1.0", "@noble/curves@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" @@ -4638,12 +4644,12 @@ dependencies: "@noble/hashes" "1.5.0" -"@noble/curves@^1.6.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" - integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== +"@noble/curves@^1.6.0", "@noble/curves@^1.8.0": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.1.tgz#19bc3970e205c99e4bdb1c64a4785706bce497ff" + integrity sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ== dependencies: - "@noble/hashes" "1.6.0" + "@noble/hashes" "1.7.1" "@noble/curves@~1.4.0": version "1.4.2" @@ -4677,15 +4683,10 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== -"@noble/hashes@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" - integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== - -"@noble/hashes@^1.5.0": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" - integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== +"@noble/hashes@1.7.1", "@noble/hashes@^1.5.0", "@noble/hashes@^1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== "@noble/secp256k1@1.7.1", "@noble/secp256k1@^1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" @@ -5235,6 +5236,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== +"@scure/base@1.2.4", "@scure/base@^1.1.8", "@scure/base@~1.2.1", "@scure/base@~1.2.2": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.4.tgz#002eb571a35d69bdb4c214d0995dff76a8dcd2a9" + integrity sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ== + "@scure/base@^1.1.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" @@ -5245,11 +5251,6 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== -"@scure/base@^1.1.8": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" - integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== - "@scure/base@~1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" @@ -8660,9 +8661,9 @@ bufferutil@^4.0.1: node-gyp-build "^4.3.0" bufferutil@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" - integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== + version "4.0.9" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.9.tgz#6e81739ad48a95cad45a279588e13e95e24a800a" + integrity sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw== dependencies: node-gyp-build "^4.3.0" @@ -9699,11 +9700,11 @@ cross-fetch@3.1.5, cross-fetch@^3.1.4: node-fetch "2.6.7" cross-fetch@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" - integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + version "4.1.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.1.0.tgz#8f69355007ee182e47fa692ecbaa37a52e43c3d2" + integrity sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw== dependencies: - node-fetch "^2.6.12" + node-fetch "^2.7.0" cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" @@ -10794,9 +10795,9 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.0, end-of-stream@ once "^1.4.0" engine.io-client@~6.6.1: - version "6.6.2" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.2.tgz#e0a09e1c90effe5d6264da1c56d7281998f1e50b" - integrity sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw== + version "6.6.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.3.tgz#815393fa24f30b8e6afa8f77ccca2f28146be6de" + integrity sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" @@ -16163,6 +16164,17 @@ micro-ftch@^0.3.1: resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== +micro-key-producer@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/micro-key-producer/-/micro-key-producer-0.7.5.tgz#1418ef2190a9aeb2a4e17259b4a4968e02f5b683" + integrity sha512-pFVPJVr3yrxr03qeplzPEfIsiBgBTJiffx75jFKESAl9kyGWE7hx4B348znT8Ey1e/K1Z+WzQLjUay90yVVQGA== + dependencies: + "@noble/ciphers" "^1.2.0" + "@noble/curves" "^1.8.0" + "@noble/hashes" "^1.7.0" + "@scure/base" "~1.2.1" + micro-packed "~0.7.1" + micro-packed@~0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.5.2.tgz#5537f97f74fe00d3b3d761d697bb38b9d96d241e" @@ -16177,6 +16189,13 @@ micro-packed@~0.6.2: dependencies: "@scure/base" "~1.1.5" +micro-packed@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.7.2.tgz#7f9decd6c11fe2617bc85ad4ebc0ad48bf423f36" + integrity sha512-HJ/u8+tMzgrJVAl6P/4l8KGjJSA3SCZaRb1m4wpbovNScCSmVOGUYbkkcoPPcknCHWPpRAdjy+yqXqyQWf+k8g== + dependencies: + "@scure/base" "~1.2.2" + micro-signals@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/micro-signals/-/micro-signals-2.4.0.tgz#007af19ea18051e360f9fa17023284779d839593" @@ -16633,7 +16652,7 @@ node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.12: +node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -17701,6 +17720,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatc resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -18601,9 +18625,9 @@ q@^1.1.2, q@^1.5.1: integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== qr-code-styling@^1.6.0-rc.1: - version "1.8.4" - resolved "https://registry.yarnpkg.com/qr-code-styling/-/qr-code-styling-1.8.4.tgz#9168f379cc8f239c184951d5c1ad8a32ad0b19f9" - integrity sha512-uxykNuvXaPDK/jGDERDIdDvvocefbHu1oxVYi6K87FUdPPAezkBdcIeFJ8XVX2HSsyLFINile5uzfOMYpGu5ZA== + version "1.9.1" + resolved "https://registry.yarnpkg.com/qr-code-styling/-/qr-code-styling-1.9.1.tgz#1fc27ccb6d42051225f5f18ab9b99307ad62ef8b" + integrity sha512-T/VxQchuZkQwYhIcyyMUmtXHPeDT6lJBYHfqGD5CBDyIjswxS7JZKf443q+SXO1K/9SUswi6JpXEUQ5AoMCpyg== dependencies: qrcode-generator "^1.4.4" @@ -19210,9 +19234,9 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable util-deprecate "~1.0.1" "readable-stream@^3.6.2 || ^4.4.2": - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== dependencies: abort-controller "^3.0.0" buffer "^6.0.3" @@ -19666,12 +19690,12 @@ rollup-plugin-terser@^7.0.0: terser "^5.0.0" rollup-plugin-visualizer@^5.9.2: - version "5.12.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz#661542191ce78ee4f378995297260d0c1efb1302" - integrity sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ== + version "5.14.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz#be82d43fb3c644e396e2d50ac8a53d354022d57c" + integrity sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA== dependencies: open "^8.4.0" - picomatch "^2.3.1" + picomatch "^4.0.2" source-map "^0.7.4" yargs "^17.5.1"