diff --git a/src/__test__/e2e/api.test.ts b/src/__test__/e2e/api.test.ts index 0078c5dc..140cf98e 100644 --- a/src/__test__/e2e/api.test.ts +++ b/src/__test__/e2e/api.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable quotes */ import { getClient } from './../../api/utilities'; import { Chain, Common, Hardfork } from '@ethereumjs/common'; import { TransactionFactory } from '@ethereumjs/tx'; @@ -7,8 +8,11 @@ import { fetchActiveWallets, fetchAddress, fetchAddresses, + fetchBip44ChangeAddresses, fetchBtcLegacyAddresses, fetchBtcSegwitAddresses, + fetchByDerivationPath, + fetchSolanaAddresses, pair, signBtcLegacyTx, signBtcSegwitTx, @@ -184,6 +188,79 @@ describe('API', () => { const addresses = await fetchLedgerLiveAddresses(); expect(addresses).toHaveLength(10); }); + + test('fetchSolanaAddresses', async () => { + const addresses = await fetchSolanaAddresses(); + expect(addresses).toHaveLength(10); + }); + + test('fetchBip44ChangeAddresses', async () => { + const addresses = await fetchBip44ChangeAddresses(); + expect(addresses).toHaveLength(10); + }); + }); + + describe('fetchByDerivationPath', () => { + test('fetch single specific address', async () => { + const addresses = await fetchByDerivationPath("44'/60'/0'/0/0"); + expect(addresses).toHaveLength(1); + console.log(addresses[0]); + expect(addresses[0]).toBeTruthy(); + }); + + test('fetch multiple addresses with wildcard', async () => { + const addresses = await fetchByDerivationPath("44'/60'/0'/0/X", { + n: 5, + }); + console.log(addresses[0]); + expect(addresses).toHaveLength(5); + addresses.forEach((address) => expect(address).toBeTruthy()); + }); + + test('fetch addresses with offset', async () => { + const addresses = await fetchByDerivationPath("44'/60'/0'/0/X", { + n: 3, + startPathIndex: 10, + }); + console.log(addresses[0]); + expect(addresses).toHaveLength(3); + addresses.forEach((address) => expect(address).toBeTruthy()); + }); + + test('fetch addresses with lowercase x wildcard', async () => { + const addresses = await fetchByDerivationPath("44'/60'/0'/0/x", { + n: 2, + }); + expect(addresses).toHaveLength(2); + addresses.forEach((address) => expect(address).toBeTruthy()); + }); + + test('fetch addresses with wildcard in middle of path', async () => { + const addresses = await fetchByDerivationPath("44'/60'/X'/0/0", { + n: 3, + }); + expect(addresses).toHaveLength(3); + addresses.forEach((address) => expect(address).toBeTruthy()); + }); + + test('error on invalid derivation path', async () => { + await expect(fetchByDerivationPath('invalid/path')).rejects.toThrow(); + }); + + test('fetch single address when n=1 with wildcard', async () => { + const addresses = await fetchByDerivationPath("44'/60'/0'/0/X", { + n: 1, + }); + expect(addresses).toHaveLength(1); + expect(addresses[0]).toBeTruthy(); + }); + + test('fetch no addresses when n=0', async () => { + const addresses = await fetchByDerivationPath("44'/60'/0'/0/X", { + n: 0, + }); + expect(addresses).toHaveLength(0); + }); }); describe('fetchAddress', () => { diff --git a/src/api/addresses.ts b/src/api/addresses.ts index 0ff974b7..2224890b 100644 --- a/src/api/addresses.ts +++ b/src/api/addresses.ts @@ -3,6 +3,7 @@ import { BTC_SEGWIT_DERIVATION, BTC_WRAPPED_SEGWIT_DERIVATION, DEFAULT_ETH_DERIVATION, + HARDENED_OFFSET, LEDGER_LEGACY_DERIVATION, LEDGER_LIVE_DERIVATION, MAX_ADDR, @@ -105,6 +106,7 @@ export const fetchSolanaAddresses = async ( return fetchAddresses({ startPath: getStartPath(SOLANA_DERIVATION, startPathIndex, 2), n, + flag: 4, }); }; @@ -159,3 +161,80 @@ export const fetchLedgerLegacyAddresses = async ( } return Promise.all(addresses); }; + +export const fetchBip44ChangeAddresses = async ( + { n, startPathIndex }: FetchAddressesParams = { + n: MAX_ADDR, + startPathIndex: 0, + }, +) => { + const addresses = []; + for (let i = 0; i < n; i++) { + addresses.push( + queue((client) => { + const startPath = [ + 44 + HARDENED_OFFSET, + 501 + HARDENED_OFFSET, + startPathIndex + i + HARDENED_OFFSET, + 0 + HARDENED_OFFSET, + ]; + return client + .getAddresses({ + startPath, + n: 1, + flag: 4, + }) + .then((addresses) => addresses.map((address) => `${address}`)); + }), + ); + } + return Promise.all(addresses); +}; + +function parseDerivationPath(path: string): number[] { + return path.split('/').map((part) => { + // eslint-disable-next-line quotes + if (part.endsWith("'")) { + return parseInt(part.slice(0, -1)) + 0x80000000; + } + return part.toLowerCase() === 'x' ? 0 : parseInt(part); + }); +} + +export async function fetchAddressesByDerivationPath( + path: string, + { n = 1, startPathIndex = 0 }: FetchAddressesParams = {}, +): Promise { + const parsedPath = parseDerivationPath(path); + const hasWildcard = path.toLowerCase().includes('x'); + + if (!hasWildcard) { + return queue((client) => + client.getAddresses({ + startPath: parsedPath, + n: 1, + }), + ); + } + + const wildcardIndex = parsedPath.findIndex((part) => part === 0); + const basePath = parsedPath.slice(0, wildcardIndex); + + const addresses: string[] = []; + for (let i = 0; i < n; i++) { + const currentPath = [ + ...basePath, + startPathIndex + i, + ...parsedPath.slice(wildcardIndex + 1), + ]; + const result = await queue((client) => + client.getAddresses({ + startPath: currentPath, + n: 1, + }), + ); + addresses.push(...result); + } + + return addresses; +} diff --git a/src/constants.ts b/src/constants.ts index 9a07248a..21316129 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -544,6 +544,7 @@ export const SOLANA_DERIVATION = [ HARDENED_OFFSET + 44, HARDENED_OFFSET + 501, HARDENED_OFFSET, + HARDENED_OFFSET, ]; /** @internal */