diff --git a/package.json b/package.json index 74337d87..01268fa8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "e2e-sign-unformatted": "vitest src/__test__/e2e/signing/unformatted.test.ts", "e2e-wj": "vitest src/__test__/e2e/wallet-jobs.test.ts", "e2e-api": "vitest src/__test__/e2e/api.test.ts", + "e2e-iter": "vitest src/__test__/e2e/iter.test.ts", "contracts": "vitest src/__test__/e2e/contracts.test.ts" }, "files": [ diff --git a/src/__test__/e2e/iter.test.ts b/src/__test__/e2e/iter.test.ts new file mode 100644 index 00000000..ff5aa048 --- /dev/null +++ b/src/__test__/e2e/iter.test.ts @@ -0,0 +1,21 @@ +import { question } from 'readline-sync'; +import { fetchAddresses, pair } from '../../api'; +import { setupClient } from '../utils/setup'; + +describe('address fetching', () => { + test('pair', async () => { + const isPaired = await setupClient(); + if (!isPaired) { + const secret = question('Please enter the pairing secret: '); + await pair(secret.toUpperCase()); + } + }); + + describe('iteration index', () => { + test('different indexes should not have the same addresses', async () => { + const addresses1 = await fetchAddresses({ iterIdx: 1 }); + const addresses2 = await fetchAddresses({ iterIdx: 2 }); + expect(addresses1).not.toEqual(addresses2); + }); + }); +}); diff --git a/src/__test__/unit/__snapshots__/encoders.test.ts.snap b/src/__test__/unit/__snapshots__/encoders.test.ts.snap index d6641ded..2d5bb443 100644 --- a/src/__test__/unit/__snapshots__/encoders.test.ts.snap +++ b/src/__test__/unit/__snapshots__/encoders.test.ts.snap @@ -10,7 +10,7 @@ exports[`encoders > getAddresses > encodeGetAddressesRequest with ED25519_PUB 1` exports[`encoders > getAddresses > encodeGetAddressesRequest with SECP256K1_PUB 1`] = `"162b56efe561c12bc93f703dc7026b3ec3d53923270c9259e2b08015fb9defd2058000002c8000003c80000000000000000000000031"`; -exports[`encoders > getAddresses > encodeGetAddressesRequest with default flag 1`] = `"162b56efe561c12bc93f703dc7026b3ec3d53923270c9259e2b08015fb9defd2058000002c8000003c80000000000000000000000001"`; +exports[`encoders > getAddresses > encodeGetAddressesRequest with default flag 1`] = `"162b56efe561c12bc93f703dc7026b3ec3d53923270c9259e2b08015fb9defd2058000002c8000003c80000000000000000000000011"`; exports[`encoders > pair > pair encoder 1`] = `"11111111111111111111111111111111"`; diff --git a/src/client.ts b/src/client.ts index 51d46f27..adb79077 100644 --- a/src/client.ts +++ b/src/client.ts @@ -180,8 +180,9 @@ export class Client { startPath, n = 1, flag = 0, + iterIdx = 0, }: GetAddressesRequestParams): Promise { - return this.retryWrapper(getAddresses, { startPath, n, flag }); + return this.retryWrapper(getAddresses, { startPath, n, flag, iterIdx }); } /** diff --git a/src/functions/getAddresses.ts b/src/functions/getAddresses.ts index 7444c2f6..51109e32 100644 --- a/src/functions/getAddresses.ts +++ b/src/functions/getAddresses.ts @@ -1,5 +1,3 @@ -import bitwise from 'bitwise'; -import { Byte, UInt4 } from 'bitwise/types'; import { LatticeGetAddressesFlag, LatticeSecureEncryptedRequestType, @@ -26,6 +24,7 @@ export async function getAddresses({ startPath: _startPath, n: _n, flag: _flag, + iterIdx, }: GetAddressesRequestFunctionParams): Promise { const { url, sharedSecret, ephemeralPub, fwConstants } = validateConnectedClient(client); @@ -43,6 +42,7 @@ export async function getAddresses({ flag, fwConstants, wallet: activeWallet, + iterIdx, }); const { decryptedData, newEphemeralPub } = await encryptedSecureRequest({ @@ -82,12 +82,14 @@ export const encodeGetAddressesRequest = ({ flag, fwConstants, wallet, + iterIdx, }: { startPath: number[]; n: number; flag: number; fwConstants: FirmwareConstants; wallet: Wallet; + iterIdx?: number; }) => { const flags = fwConstants.getAddressFlags || ([] as any[]); const isPubkeyOnly = @@ -100,54 +102,49 @@ export const encodeGetAddressesRequest = ({ 'Derivation path or flag is not supported. Try updating Lattice firmware.', ); } - let sz = 32 + 20 + 1; // walletUID + 5 u32 indices + count/flag - if (fwConstants.varAddrPathSzAllowed) { - sz += 1; // pathDepth - } else if (startPath.length !== 5) { - throw new Error( - 'Your Lattice firmware only supports derivation paths with 5 indices. Please upgrade.', - ); + + // Ensure path depth is valid (2-5 indices) + if (startPath.length < 2 || startPath.length > 5) { + throw new Error('Derivation path must include 2-5 indices.'); + } + + // Validate iterIdx (0-5) + if (iterIdx < 0 || iterIdx > 5) { + throw new Error('Iteration index must be between 0 and 5.'); } + + // Ensure iterIdx is not greater than path depth + if (iterIdx > startPath.length) { + throw new Error('Iteration index cannot be greater than path depth.'); + } + + const sz = 32 + 1 + 20 + 1; // walletUID + pathDepth_IterIdx + 5 u32 indices + count/flag const payload = Buffer.alloc(sz); let off = 0; + + // walletUID wallet.uid.copy(payload, off); off += 32; + + // pathDepth_IterIdx + const pathDepth_IterIdx = ((iterIdx & 0x0f) << 4) | (startPath.length & 0x0f); + payload.writeUInt8(pathDepth_IterIdx, off); + off += 1; + // Build the start path (5x u32 indices) - if (fwConstants.varAddrPathSzAllowed) { - payload.writeUInt8(startPath.length, off); - off += 1; - } for (let i = 0; i < 5; i++) { - if (i <= startPath.length) { - const val = startPath[i] ?? 0; - payload.writeUInt32BE(val, off); - } + const val = i < startPath.length ? startPath[i] : 0; + payload.writeUInt32BE(val, off); off += 4; } - // Specify the number of subsequent addresses to request. We also allow the user to skip the - // cache and request any address related to the asset in the wallet. - let val, - flagVal: UInt4 = 0; - if (fwConstants.addrFlagsAllowed) { - // A 4-bit flag can be used for non-standard address requests Client needs to be combined with - // `n` as a 4 bit value - flagVal = - fwConstants.getAddressFlags && - fwConstants.getAddressFlags.indexOf(flag) > -1 - ? (flag as UInt4) - : 0; - const flagBits = bitwise.nibble.read(flagVal); - const countBits = bitwise.nibble.read(n as UInt4); - val = bitwise.byte.write(flagBits.concat(countBits) as Byte); - } else { - // Very old firmware does not support client flag. We can deprecate client soon. - val = n; - } - payload.writeUInt8(val, off); - off++; + + // Combine count and flag into a single byte + const countVal = n & 0x0f; + const flagVal = (flag & 0x0f) << 4; + payload.writeUInt8(countVal | flagVal, off); + return payload; }; - /** * @internal * @return an array of address strings or pubkey buffers diff --git a/src/types/getAddresses.d.ts b/src/types/getAddresses.d.ts index 5f78734e..b266389a 100644 --- a/src/types/getAddresses.d.ts +++ b/src/types/getAddresses.d.ts @@ -2,6 +2,7 @@ interface GetAddressesRequestParams { startPath: number[]; n: number; flag?: number; + iterIdx?: number; } interface GetAddressesRequestFunctionParams extends GetAddressesRequestParams {