diff --git a/README.md b/README.md index 0e4aba4e..4fb7302a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ linkStyle default opacity:0.5 eth_hd_keyring --> keyring_utils; eth_hd_keyring --> account_api; eth_ledger_bridge_keyring --> keyring_utils; + eth_qr_keyring --> keyring_api; eth_qr_keyring --> keyring_utils; + eth_qr_keyring --> account_api; eth_simple_keyring --> keyring_api; eth_simple_keyring --> keyring_utils; eth_trezor_keyring --> keyring_utils; diff --git a/packages/keyring-eth-qr/CHANGELOG.md b/packages/keyring-eth-qr/CHANGELOG.md index a4ed08a0..e0b41a18 100644 --- a/packages/keyring-eth-qr/CHANGELOG.md +++ b/packages/keyring-eth-qr/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `QrKeyringV2` class implementing `KeyringV2` interface ([#411](https://github.com/MetaMask/accounts/pull/411)) + - Wraps legacy `QrKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type. + - Extends `EthKeyringWrapper` for common Ethereum logic. + ## [1.1.0] ### Added diff --git a/packages/keyring-eth-qr/package.json b/packages/keyring-eth-qr/package.json index 080ecb5d..b22df5a0 100644 --- a/packages/keyring-eth-qr/package.json +++ b/packages/keyring-eth-qr/package.json @@ -53,6 +53,7 @@ "@ethereumjs/util": "^9.1.0", "@keystonehq/bc-ur-registry-eth": "^0.19.1", "@metamask/eth-sig-util": "^8.2.0", + "@metamask/keyring-api": "workspace:^", "@metamask/keyring-utils": "workspace:^", "@metamask/utils": "^11.1.0", "async-mutex": "^0.5.0", @@ -63,6 +64,7 @@ "@ethereumjs/common": "^4.4.0", "@keystonehq/metamask-airgapped-keyring": "^0.15.2", "@lavamoat/allow-scripts": "^3.2.1", + "@metamask/account-api": "workspace:^", "@metamask/auto-changelog": "^3.4.4", "@types/hdkey": "^2.0.1", "@types/jest": "^29.5.12", diff --git a/packages/keyring-eth-qr/src/index.ts b/packages/keyring-eth-qr/src/index.ts index 36a60d85..3fdfed9e 100644 --- a/packages/keyring-eth-qr/src/index.ts +++ b/packages/keyring-eth-qr/src/index.ts @@ -17,3 +17,4 @@ export { type SerializedQrKeyringState, type SerializedUR, } from './qr-keyring'; +export { QrKeyringV2, type QrKeyringV2Options } from './qr-keyring-v2'; diff --git a/packages/keyring-eth-qr/src/qr-keyring-v2.test.ts b/packages/keyring-eth-qr/src/qr-keyring-v2.test.ts new file mode 100644 index 00000000..5120e7d2 --- /dev/null +++ b/packages/keyring-eth-qr/src/qr-keyring-v2.test.ts @@ -0,0 +1,487 @@ +import type { Bip44Account } from '@metamask/account-api'; +import { + EthAccountType, + EthMethod, + EthScope, + type KeyringAccount, + KeyringAccountEntropyTypeOption, + KeyringType, +} from '@metamask/keyring-api'; + +import type { QrKeyringBridge } from '.'; +import { QrKeyring } from '.'; +import { QrKeyringV2 } from './qr-keyring-v2'; +import { + ACCOUNT_SERIALIZED_KEYRING_WITH_ACCOUNTS, + EXPECTED_ACCOUNTS, + HDKEY_SERIALIZED_KEYRING_WITH_ACCOUNTS, + HDKEY_SERIALIZED_KEYRING_WITH_NO_ACCOUNTS, + KNOWN_CRYPTO_ACCOUNT_UR, + KNOWN_HDKEY_UR, +} from '../test/fixtures'; + +/** + * Type alias for QR keyring accounts (always BIP-44 derived). + */ +type QrAccount = Bip44Account; + +/** + * Get the first account from an array, throwing if empty. + * + * @param accounts - The accounts array. + * @returns The first account. + */ +function getFirstAccount(accounts: QrAccount[]): QrAccount { + if (accounts.length === 0) { + throw new Error('Expected at least one account'); + } + return accounts[0] as QrAccount; +} + +/** + * Get an account at a specific index, throwing if not present. + * + * @param accounts - The accounts array. + * @param index - The index to retrieve. + * @returns The account at the index. + */ +function getAccountAt(accounts: QrAccount[], index: number): QrAccount { + if (accounts.length <= index) { + throw new Error(`Expected account at index ${index}`); + } + return accounts[index] as QrAccount; +} + +const entropySource = HDKEY_SERIALIZED_KEYRING_WITH_NO_ACCOUNTS.xfp; + +/** + * Expected methods supported by QR keyring accounts. + */ +const EXPECTED_METHODS = [ + EthMethod.SignTransaction, + EthMethod.PersonalSign, + EthMethod.SignTypedDataV4, +]; + +/** + * Get a mock bridge for the QrKeyring. + * + * @returns A mock bridge with a requestScan method. + */ +function getMockBridge(): QrKeyringBridge { + return { + requestScan: jest.fn(), + }; +} + +/** + * Create a fresh QrKeyring with HD mode device. + * + * @returns The inner keyring. + */ +function createInnerKeyring(): QrKeyring { + return new QrKeyring({ + bridge: getMockBridge(), + ur: KNOWN_HDKEY_UR, + }); +} + +/** + * Create a QrKeyringV2 wrapper with a paired HD mode device and accounts. + * + * @param accountCount - Number of accounts to add. + * @returns The wrapper and inner keyring. + */ +async function createWrapperWithAccounts(accountCount = 3): Promise<{ + wrapper: QrKeyringV2; + inner: QrKeyring; +}> { + const inner = createInnerKeyring(); + await inner.addAccounts(accountCount); + + const wrapper = new QrKeyringV2({ legacyKeyring: inner, entropySource }); + return { wrapper, inner }; +} + +/** + * Create a QrKeyringV2 wrapper without any accounts. + * + * @returns The wrapper and inner keyring. + */ +function createEmptyWrapper(): { wrapper: QrKeyringV2; inner: QrKeyring } { + const inner = createInnerKeyring(); + const wrapper = new QrKeyringV2({ legacyKeyring: inner, entropySource }); + return { wrapper, inner }; +} + +/** + * Create a QrKeyringV2 wrapper with a paired Account mode device. + * + * @returns The wrapper and inner keyring. + */ +async function createAccountModeWrapper(): Promise<{ + wrapper: QrKeyringV2; + inner: QrKeyring; +}> { + const inner = new QrKeyring({ + bridge: getMockBridge(), + ur: KNOWN_CRYPTO_ACCOUNT_UR, + }); + await inner.addAccounts(1); + + const wrapper = new QrKeyringV2({ + legacyKeyring: inner, + entropySource: ACCOUNT_SERIALIZED_KEYRING_WITH_ACCOUNTS.xfp, + }); + return { wrapper, inner }; +} + +/** + * Helper to create account options for bip44:derive-index. + * + * @param groupIndex - The group index to derive. + * @param source - Optional entropy source override. + * @returns The create account options. + */ +function deriveIndexOptions( + groupIndex: number, + source: string = entropySource, +): { + type: 'bip44:derive-index'; + entropySource: string; + groupIndex: number; +} { + return { + type: 'bip44:derive-index' as const, + entropySource: source, + groupIndex, + }; +} + +describe('QrKeyringV2', () => { + describe('constructor', () => { + it('creates a wrapper with correct type and capabilities', () => { + const { wrapper } = createEmptyWrapper(); + + expect(wrapper.type).toBe(KeyringType.Qr); + expect(wrapper.capabilities).toStrictEqual({ + scopes: [EthScope.Eoa], + bip44: { + deriveIndex: true, + derivePath: false, + discover: false, + }, + }); + }); + }); + + describe('getAccounts', () => { + it('returns empty array when no device is paired', async () => { + const inner = new QrKeyring({ bridge: getMockBridge() }); + const wrapper = new QrKeyringV2({ legacyKeyring: inner, entropySource }); + + const accounts = await wrapper.getAccounts(); + + expect(accounts).toStrictEqual([]); + }); + + it('returns all accounts from the legacy keyring', async () => { + const { wrapper } = await createWrapperWithAccounts(3); + + const accounts = await wrapper.getAccounts(); + + expect(accounts).toHaveLength(3); + expect(accounts.map((a) => a.address)).toStrictEqual( + EXPECTED_ACCOUNTS.slice(0, 3), + ); + }); + + it('creates KeyringAccount objects with correct structure', async () => { + const { wrapper } = await createWrapperWithAccounts(1); + + const accounts = await wrapper.getAccounts(); + const account = getFirstAccount(accounts); + + expect(account).toMatchObject({ + type: EthAccountType.Eoa, + address: EXPECTED_ACCOUNTS[0], + scopes: [EthScope.Eoa], + methods: EXPECTED_METHODS, + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.Mnemonic, + id: entropySource, + groupIndex: 0, + derivationPath: `m/44'/60'/0'/0/0`, + }, + }, + }); + expect(account.id).toBeDefined(); + }); + + it('returns correct groupIndex for multiple accounts', async () => { + const { wrapper } = await createWrapperWithAccounts(3); + + const accounts = await wrapper.getAccounts(); + + accounts.forEach((account, index) => { + expect(account.options.entropy.groupIndex).toBe(index); + }); + }); + + it('throws error in HD mode when address is not in indexes map', async () => { + const { wrapper, inner } = await createWrapperWithAccounts(1); + + // Mock serialize to return an inconsistent state + jest.spyOn(inner, 'serialize').mockResolvedValue({ + ...HDKEY_SERIALIZED_KEYRING_WITH_ACCOUNTS, + indexes: {}, // Empty indexes - inconsistent with accounts + }); + + await expect(wrapper.getAccounts()).rejects.toThrow( + /not found in device indexes/u, + ); + }); + + it('throws error when getPathFromAddress returns undefined', async () => { + const { wrapper, inner } = await createWrapperWithAccounts(1); + + jest.spyOn(inner, 'getPathFromAddress').mockReturnValue(undefined); + + await expect(wrapper.getAccounts()).rejects.toThrow( + /derivation path not found in keyring/u, + ); + }); + }); + + describe('deserialize', () => { + it('deserializes the legacy keyring state', async () => { + const inner = new QrKeyring({ bridge: getMockBridge() }); + const wrapper = new QrKeyringV2({ legacyKeyring: inner, entropySource }); + + await wrapper.deserialize(HDKEY_SERIALIZED_KEYRING_WITH_ACCOUNTS); + + const accounts = await wrapper.getAccounts(); + expect(accounts).toHaveLength(3); + expect(accounts.map((a) => a.address)).toStrictEqual( + EXPECTED_ACCOUNTS.slice(0, 3), + ); + }); + + it('clears the cache and rebuilds it', async () => { + const { wrapper } = await createWrapperWithAccounts(2); + + const accountsBefore = await wrapper.getAccounts(); + expect(accountsBefore).toHaveLength(2); + + await wrapper.deserialize(HDKEY_SERIALIZED_KEYRING_WITH_ACCOUNTS); + + const accountsAfter = await wrapper.getAccounts(); + expect(accountsAfter).toHaveLength(3); + // The accounts should be new objects (cache was cleared) + expect(accountsAfter[0]).not.toBe(accountsBefore[0]); + }); + }); + + describe('createAccounts', () => { + it('creates the first account at index 0', async () => { + const { wrapper } = createEmptyWrapper(); + + const newAccounts = await wrapper.createAccounts(deriveIndexOptions(0)); + const account = getFirstAccount(newAccounts); + + expect(account.address).toBe(EXPECTED_ACCOUNTS[0]); + }); + + it('creates an account at a specific index', async () => { + const { wrapper } = await createWrapperWithAccounts(2); + + const newAccounts = await wrapper.createAccounts(deriveIndexOptions(2)); + const account = getFirstAccount(newAccounts); + + expect(account.address).toBe(EXPECTED_ACCOUNTS[2]); + }); + + it('returns existing account if groupIndex already exists', async () => { + const { wrapper } = await createWrapperWithAccounts(2); + + const existingAccounts = await wrapper.getAccounts(); + const existingAccount = getFirstAccount(existingAccounts); + const newAccounts = await wrapper.createAccounts(deriveIndexOptions(0)); + const returnedAccount = getFirstAccount(newAccounts); + + expect(returnedAccount).toBe(existingAccount); + }); + + it('throws error for unsupported account creation type', async () => { + const { wrapper } = await createWrapperWithAccounts(0); + + await expect( + wrapper.createAccounts({ + type: 'private-key:import', + accountType: EthAccountType.Eoa, + encoding: 'hexadecimal', + privateKey: '0xabc', + }), + ).rejects.toThrow( + 'Unsupported account creation type for QrKeyring: private-key:import', + ); + }); + + it('throws error for entropy source mismatch', async () => { + const { wrapper } = await createWrapperWithAccounts(0); + + await expect( + wrapper.createAccounts(deriveIndexOptions(0, 'wrong-entropy')), + ).rejects.toThrow(/Entropy source mismatch/u); + }); + + it('throws error when no device is paired', async () => { + const inner = new QrKeyring({ bridge: getMockBridge() }); + const wrapper = new QrKeyringV2({ legacyKeyring: inner, entropySource }); + + await expect( + wrapper.createAccounts(deriveIndexOptions(0, 'some-entropy')), + ).rejects.toThrow('No device paired. Cannot create accounts.'); + }); + + it('allows deriving accounts at any index (non-sequential)', async () => { + const { wrapper } = createEmptyWrapper(); + + const newAccounts = await wrapper.createAccounts(deriveIndexOptions(5)); + const account = getFirstAccount(newAccounts); + + expect(account.address).toBe(EXPECTED_ACCOUNTS[5]); + expect(account.options.entropy.groupIndex).toBe(5); + }); + + it('creates multiple accounts sequentially', async () => { + const { wrapper } = createEmptyWrapper(); + + const results = await Promise.all([ + wrapper.createAccounts(deriveIndexOptions(0)), + wrapper.createAccounts(deriveIndexOptions(1)), + wrapper.createAccounts(deriveIndexOptions(2)), + ]); + + results.forEach((accounts, index) => { + const account = getFirstAccount(accounts); + expect(account.address).toBe(EXPECTED_ACCOUNTS[index]); + }); + }); + + it('throws error when inner keyring fails to create account', async () => { + const { wrapper, inner } = createEmptyWrapper(); + + jest.spyOn(inner, 'addAccounts').mockResolvedValueOnce([]); + + await expect( + wrapper.createAccounts(deriveIndexOptions(0)), + ).rejects.toThrow('Failed to create new account'); + }); + }); + + describe('deleteAccount', () => { + it('removes an account from the keyring', async () => { + const { wrapper } = await createWrapperWithAccounts(3); + + const accountsBefore = await wrapper.getAccounts(); + const accountToDelete = getAccountAt(accountsBefore, 1); + + await wrapper.deleteAccount(accountToDelete.id); + + const accountsAfter = await wrapper.getAccounts(); + expect(accountsAfter).toHaveLength(2); + expect(accountsAfter.map((a) => a.address)).not.toContain( + accountToDelete.address, + ); + }); + + it('removes the account from the cache', async () => { + const { wrapper } = await createWrapperWithAccounts(2); + + const accounts = await wrapper.getAccounts(); + const accountToDelete = getFirstAccount(accounts); + + await wrapper.deleteAccount(accountToDelete.id); + + await expect(wrapper.getAccount(accountToDelete.id)).rejects.toThrow( + /Account not found/u, + ); + }); + + it('throws error for non-existent account', async () => { + const { wrapper } = await createWrapperWithAccounts(1); + + await expect(wrapper.deleteAccount('non-existent-id')).rejects.toThrow( + /Account not found/u, + ); + }); + }); + + describe('getAccount', () => { + it('returns the account by ID', async () => { + const { wrapper } = await createWrapperWithAccounts(2); + + const accounts = await wrapper.getAccounts(); + const expectedAccount = getFirstAccount(accounts); + const account = await wrapper.getAccount(expectedAccount.id); + + expect(account).toBe(expectedAccount); + }); + + it('throws error for non-existent account', async () => { + const { wrapper } = await createWrapperWithAccounts(1); + + await expect(wrapper.getAccount('non-existent-id')).rejects.toThrow( + /Account not found/u, + ); + }); + }); + + describe('serialize', () => { + it('serializes the legacy keyring state', async () => { + const { wrapper, inner } = await createWrapperWithAccounts(2); + + const wrapperSerialized = await wrapper.serialize(); + const innerSerialized = await inner.serialize(); + + expect(wrapperSerialized).toStrictEqual(innerSerialized); + }); + }); + + describe('Account Mode (CryptoAccount)', () => { + describe('getAccounts', () => { + it('returns accounts with Mnemonic entropy type (BIP-44 derived)', async () => { + const { wrapper } = await createAccountModeWrapper(); + + const accounts = await wrapper.getAccounts(); + const account = getFirstAccount(accounts); + + expect(account.address).toBe( + ACCOUNT_SERIALIZED_KEYRING_WITH_ACCOUNTS.accounts[0], + ); + expect(account.options.entropy).toMatchObject({ + type: KeyringAccountEntropyTypeOption.Mnemonic, + id: ACCOUNT_SERIALIZED_KEYRING_WITH_ACCOUNTS.xfp, + groupIndex: 0, + }); + expect(account.options.entropy.derivationPath).toBeDefined(); + }); + }); + + describe('deleteAccount', () => { + it('removes an account from the keyring', async () => { + const { wrapper, inner } = await createAccountModeWrapper(); + + const accounts = await wrapper.getAccounts(); + const account = getFirstAccount(accounts); + + await wrapper.deleteAccount(account.id); + + const remainingAddresses = await inner.getAccounts(); + expect(remainingAddresses).toHaveLength(0); + }); + }); + }); +}); diff --git a/packages/keyring-eth-qr/src/qr-keyring-v2.ts b/packages/keyring-eth-qr/src/qr-keyring-v2.ts new file mode 100644 index 00000000..ee3001f8 --- /dev/null +++ b/packages/keyring-eth-qr/src/qr-keyring-v2.ts @@ -0,0 +1,236 @@ +import type { Bip44Account } from '@metamask/account-api'; +import { + type CreateAccountOptions, + EthAccountType, + EthKeyringWrapper, + EthMethod, + EthScope, + type KeyringAccount, + KeyringAccountEntropyTypeOption, + type KeyringCapabilities, + type KeyringV2, + KeyringType, + type EntropySourceId, +} from '@metamask/keyring-api'; +import type { AccountId } from '@metamask/keyring-utils'; +import type { Hex } from '@metamask/utils'; + +import { DeviceMode } from './device'; +import type { QrKeyring } from './qr-keyring'; + +/** + * Methods supported by QR keyring EOA accounts. + * QR keyrings support a subset of signing methods (no encryption, app keys, or EIP-7702). + */ +const QR_KEYRING_METHODS = [ + EthMethod.SignTransaction, + EthMethod.PersonalSign, + EthMethod.SignTypedDataV4, +]; + +const qrKeyringV2Capabilities: KeyringCapabilities = { + scopes: [EthScope.Eoa], + bip44: { + deriveIndex: true, + derivePath: false, + discover: false, + }, +}; + +/** + * Concrete {@link KeyringV2} adapter for {@link QrKeyring}. + * + * This wrapper exposes the accounts and signing capabilities of the legacy + * QR keyring via the unified V2 interface. + * + * All QR keyring accounts are BIP-44 derived (both HD and Account modes use + * derivation paths from the hardware device). + */ +export type QrKeyringV2Options = { + legacyKeyring: QrKeyring; + entropySource: EntropySourceId; +}; + +export class QrKeyringV2 + extends EthKeyringWrapper> + implements KeyringV2 +{ + readonly entropySource: EntropySourceId; + + constructor(options: QrKeyringV2Options) { + super({ + type: KeyringType.Qr, + inner: options.legacyKeyring, + capabilities: qrKeyringV2Capabilities, + }); + this.entropySource = options.entropySource; + } + + /** + * Get the device state from the inner keyring. + * + * @returns The device state, or undefined if no device is paired. + */ + async #getDeviceState(): Promise< + { mode: DeviceMode; indexes: Record } | undefined + > { + const state = await this.inner.serialize(); + + if (!state?.initialized) { + return undefined; + } + + return { + mode: state.keyringMode, + indexes: state.indexes, + }; + } + + /** + * Creates a Bip44Account object for the given address. + * + * @param address - The account address. + * @param addressIndex - The account index in the derivation path. + * @returns The created Bip44Account. + */ + #createKeyringAccount( + address: Hex, + addressIndex: number, + ): Bip44Account { + const id = this.registry.register(address); + + const derivationPath = this.inner.getPathFromAddress(address); + + if (!derivationPath) { + throw new Error( + `Cannot create account for address ${address}: derivation path not found in keyring.`, + ); + } + + const account: Bip44Account = { + id, + type: EthAccountType.Eoa, + address, + scopes: [...this.capabilities.scopes], + methods: [...QR_KEYRING_METHODS], + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.Mnemonic, + id: this.entropySource, + groupIndex: addressIndex, + derivationPath, + }, + }, + }; + + this.registry.set(account); + return account; + } + + async getAccounts(): Promise[]> { + const addresses = await this.inner.getAccounts(); + const deviceState = await this.#getDeviceState(); + + if (!deviceState) { + // No device paired yet, return empty + return []; + } + + const { mode, indexes } = deviceState; + + return addresses.map((address, arrayIndex) => { + // Check if we already have this account in the registry + const existingId = this.registry.getAccountId(address); + if (existingId) { + const cached = this.registry.get(existingId); + if (cached) { + return cached; + } + } + + let addressIndex: number; + if (mode === DeviceMode.HD) { + // HD mode: index must be in the map + const index = indexes[address]; + if (index === undefined) { + throw new Error( + `Address ${address} not found in device indexes. This indicates an inconsistent keyring state.`, + ); + } + addressIndex = index; + } else { + // Account mode: use array position (indexes map is not populated) + addressIndex = arrayIndex; + } + + return this.#createKeyringAccount(address, addressIndex); + }); + } + + async createAccounts( + options: CreateAccountOptions, + ): Promise[]> { + return this.withLock(async () => { + const deviceState = await this.#getDeviceState(); + + if (!deviceState) { + throw new Error('No device paired. Cannot create accounts.'); + } + + // Only supports BIP-44 derive index + if (options.type !== 'bip44:derive-index') { + throw new Error( + `Unsupported account creation type for QrKeyring: ${options.type}`, + ); + } + + // Validate that the entropy source matches this keyring's entropy source + if (options.entropySource !== this.entropySource) { + throw new Error( + `Entropy source mismatch: expected '${this.entropySource}', got '${options.entropySource}'`, + ); + } + + const targetIndex = options.groupIndex; + + // Check if an account at this index already exists + const currentAccounts = await this.getAccounts(); + const existingAccount = currentAccounts.find( + (account) => account.options.entropy.groupIndex === targetIndex, + ); + if (existingAccount) { + return [existingAccount]; + } + + // Derive the account at the specified index + this.inner.setAccountToUnlock(targetIndex); + const [newAddress] = await this.inner.addAccounts(1); + + if (!newAddress) { + throw new Error('Failed to create new account'); + } + + const newAccount = this.#createKeyringAccount(newAddress, targetIndex); + + return [newAccount]; + }); + } + + /** + * Delete an account from the keyring. + * + * @param accountId - The account ID to delete. + */ + async deleteAccount(accountId: AccountId): Promise { + await this.withLock(async () => { + const { address } = await this.getAccount(accountId); + const hexAddress = this.toHexAddress(address); + + // Remove from the legacy keyring + this.inner.removeAccount(hexAddress); + + // Remove from the registry + this.registry.delete(accountId); + }); + } +} diff --git a/packages/keyring-eth-qr/src/qr-keyring.ts b/packages/keyring-eth-qr/src/qr-keyring.ts index f821ba97..d0adce91 100644 --- a/packages/keyring-eth-qr/src/qr-keyring.ts +++ b/packages/keyring-eth-qr/src/qr-keyring.ts @@ -157,6 +157,17 @@ export class QrKeyring implements Keyring { this.#accounts = (state.accounts ?? []).map(normalizeAddress); } + /** + * Get the derivation path for a given address. + * + * @param address - The address to get the derivation path for. + * @returns The derivation path for the address, or undefined if the device + * is not paired or the address is not found. + */ + getPathFromAddress(address: Hex): string | undefined { + return this.#device?.pathFromAddress(address); + } + /** * Adds accounts to the QrKeyring * diff --git a/packages/keyring-eth-qr/tsconfig.build.json b/packages/keyring-eth-qr/tsconfig.build.json index 08ef3375..7c5eabdc 100644 --- a/packages/keyring-eth-qr/tsconfig.build.json +++ b/packages/keyring-eth-qr/tsconfig.build.json @@ -9,6 +9,12 @@ "references": [ { "path": "../keyring-utils/tsconfig.build.json" + }, + { + "path": "../keyring-api/tsconfig.build.json" + }, + { + "path": "../account-api/tsconfig.build.json" } ], "include": ["./src/**/*.ts"], diff --git a/packages/keyring-eth-qr/tsconfig.json b/packages/keyring-eth-qr/tsconfig.json index f6a8db2f..50f5c182 100644 --- a/packages/keyring-eth-qr/tsconfig.json +++ b/packages/keyring-eth-qr/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "baseUrl": "./" }, - "references": [{ "path": "../keyring-utils" }], + "references": [ + { "path": "../keyring-utils" }, + { "path": "../keyring-api" }, + { "path": "../account-api" } + ], "include": ["./src", "./test"], "exclude": ["./dist/**/*"] } diff --git a/yarn.lock b/yarn.lock index 8389165d..f2e428d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1746,8 +1746,10 @@ __metadata: "@keystonehq/bc-ur-registry-eth": "npm:^0.19.1" "@keystonehq/metamask-airgapped-keyring": "npm:^0.15.2" "@lavamoat/allow-scripts": "npm:^3.2.1" + "@metamask/account-api": "workspace:^" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/keyring-api": "workspace:^" "@metamask/keyring-utils": "workspace:^" "@metamask/utils": "npm:^11.1.0" "@types/hdkey": "npm:^2.0.1"