From 388ef6393fb6e26f9472dea25c495743d3b806b1 Mon Sep 17 00:00:00 2001 From: Doug Lance Date: Mon, 27 Jun 2022 09:34:27 -0400 Subject: [PATCH] Responds to feedback from PR #413 --- src/__test__/e2e/btc.test.ts | 32 +-- src/__test__/e2e/eth.msg.test.ts | 3 +- src/__test__/e2e/general.test.ts | 23 ++- src/__test__/e2e/kv.test.ts | 3 +- src/__test__/e2e/non-exportable.test.ts | 3 +- src/__test__/e2e/signing/signing.test.ts | 4 +- src/__test__/e2e/wallet-jobs.test.ts | 187 +++++++++--------- .../__snapshots__/validators.test.ts.snap | 1 - src/__test__/utils/builders.ts | 3 +- src/__test__/utils/initializeClient.ts | 2 +- src/__test__/utils/setup.ts | 2 +- src/client.ts | 106 ++-------- src/functions/fetchActiveWallet.ts | 7 +- src/functions/getAddresses.ts | 18 +- src/shared/errors.ts | 3 +- src/shared/functions.ts | 52 +++-- src/shared/validators.ts | 3 +- 17 files changed, 206 insertions(+), 246 deletions(-) diff --git a/src/__test__/e2e/btc.test.ts b/src/__test__/e2e/btc.test.ts index ec95fbac..cbdc9f6e 100644 --- a/src/__test__/e2e/btc.test.ts +++ b/src/__test__/e2e/btc.test.ts @@ -18,7 +18,6 @@ import { testRequest } from '../utils/testRequest'; const prng = getPrng(); const TEST_TESTNET = !!getTestnet() || false; -const client = initializeClient(); let wallet: Wallet | null = null; type InputObj = { hash: string, value: number, signerIdx: number, idx: number }; @@ -41,7 +40,7 @@ for (let i = 0; i < count; i++) { inputs.push({ hash: hash.toString('hex'), value, signerIdx, idx }); } -async function testSign ({ txReq, signingKeys, sigHashes }: any) { +async function testSign ({ txReq, signingKeys, sigHashes, client }: any) { const tx = await client.sign(txReq); const len = tx?.sigs?.length ?? 0 expect(len).toEqual(signingKeys.length); @@ -63,29 +62,32 @@ async function runTestSet ( opts: any, wallet: Wallet | null, inputsSlice: InputObj[], + client, ) { expect(wallet).not.toEqualElseLog(null, 'Wallet not available'); if (TEST_TESTNET) { // Testnet + change opts.isTestnet = true; opts.useChange = true; - await testSign(setup_btc_sig_test(opts, wallet, inputsSlice, prng)); + await testSign({ ...setup_btc_sig_test(opts, wallet, inputsSlice, prng), client }); // Testnet + no change opts.isTestnet = true; opts.useChange = false; - await testSign(setup_btc_sig_test(opts, wallet, inputsSlice, prng)); + await testSign({ ...setup_btc_sig_test(opts, wallet, inputsSlice, prng), client }); } // Mainnet + change opts.isTestnet = false; opts.useChange = true; - await testSign(setup_btc_sig_test(opts, wallet, inputsSlice, prng)); + await testSign({ ...setup_btc_sig_test(opts, wallet, inputsSlice, prng), client }); // Mainnet + no change opts.isTestnet = false; opts.useChange = false; - await testSign(setup_btc_sig_test(opts, wallet, inputsSlice, prng)); + await testSign({ ...setup_btc_sig_test(opts, wallet, inputsSlice, prng), client }); } describe('Bitcoin', () => { + const client = initializeClient(); + describe('wallet seeds', () => { it('Should get GP_SUCCESS for a known, connected wallet', async () => { const activeWalletUID = client.getActiveWallet()?.uid; @@ -117,7 +119,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2WPKH, recipientPurpose: BTC_PURPOSE_P2PKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); it('p2wpkh->p2sh-p2wpkh', async () => { @@ -125,7 +127,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2WPKH, recipientPurpose: BTC_PURPOSE_P2SH_P2WPKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); it('p2wpkh->p2wpkh', async () => { @@ -133,7 +135,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2WPKH, recipientPurpose: BTC_PURPOSE_P2WPKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); }); @@ -143,7 +145,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2SH_P2WPKH, recipientPurpose: BTC_PURPOSE_P2PKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); it('p2sh-p2wpkh->p2sh-p2wpkh', async () => { @@ -151,7 +153,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2SH_P2WPKH, recipientPurpose: BTC_PURPOSE_P2SH_P2WPKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); it('p2sh-p2wpkh->p2wpkh', async () => { @@ -159,7 +161,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2SH_P2WPKH, recipientPurpose: BTC_PURPOSE_P2WPKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); }); @@ -169,7 +171,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2PKH, recipientPurpose: BTC_PURPOSE_P2PKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); it('p2pkh->p2sh-p2wpkh', async () => { @@ -177,7 +179,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2PKH, recipientPurpose: BTC_PURPOSE_P2SH_P2WPKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); it('p2pkh->p2wpkh', async () => { @@ -185,7 +187,7 @@ describe('Bitcoin', () => { spenderPurpose: BTC_PURPOSE_P2PKH, recipientPurpose: BTC_PURPOSE_P2WPKH, }; - await runTestSet(opts, wallet, inputsSlice); + await runTestSet(opts, wallet, inputsSlice, client); }); }); }) diff --git a/src/__test__/e2e/eth.msg.test.ts b/src/__test__/e2e/eth.msg.test.ts index 2f17616e..5bd209cf 100644 --- a/src/__test__/e2e/eth.msg.test.ts +++ b/src/__test__/e2e/eth.msg.test.ts @@ -23,10 +23,11 @@ import { getN } from '../utils/getters'; import { initializeClient } from '../utils/initializeClient'; import { runEthMsg } from '../utils/runners'; -const client = initializeClient(); const numRandom = getN() ? getN() : 20; // Number of random tests to conduct describe('ETH Messages', () => { + const client = initializeClient(); + describe('Test ETH personalSign', function () { it('Should throw error when message contains non-ASCII characters', async () => { const protocol = 'signPersonal'; diff --git a/src/__test__/e2e/general.test.ts b/src/__test__/e2e/general.test.ts index 563c0d9d..c7c906a4 100644 --- a/src/__test__/e2e/general.test.ts +++ b/src/__test__/e2e/general.test.ts @@ -38,9 +38,10 @@ import { import { initializeClient } from '../utils/initializeClient'; const id = getDeviceId(); -const client = initializeClient(); describe('General', () => { + const client = initializeClient(); + it('Should test SDK dehydration/rehydration', async () => { const addrData = { startPath: [BTC_PURPOSE_P2SH_P2WPKH, BTC_COIN, HARDENED_OFFSET, 0, 0], @@ -49,12 +50,14 @@ describe('General', () => { const client1 = setupTestClient(); await client1.connect(id); + expect(client1.isPaired).toBeTruthy() const addrs1 = await client1.getAddresses(addrData); const stateData = client1.getStateData(); const client2 = setupTestClient(null, stateData); await client2.connect(id); + expect(client2.isPaired).toBeTruthy() const addrs2 = await client2.getAddresses(addrData); expect(addrs1).toEqual(addrs2); @@ -145,26 +148,32 @@ describe('General', () => { await client.sign(req); }); it('should sign newer transactions', async () => { - // Test data range - const { txData, req, maxDataSz, common } = await buildEthSignRequest( + const { txData, req, common } = await buildEthSignRequest( client, + { + type: 1, + gasPrice: 1200000000, + nonce: 0, + gasLimit: 50000, + to: '0xe242e54155b1abc71fc118065270cecaaf8b7768', + value: 1000000000000, + data: '0x17e914679b7e160613be4f8c2d3203d236286d74eb9192f6d6f71b9118a42bb033ccd8e8', + } ); // NOTE: This will display a prehashed payload for bridged general signing // requests because `ethMaxDataSz` represents the `data` field for legacy // requests, but it represents the entire payload for general signing requests. - txData.data = randomBytes(maxDataSz); const tx = EthTxFactory.fromTxData(txData, { common }); req.data.payload = tx.getMessageToSign(false); await client.sign(req); }); it('should sign bad transactions', async () => { - const { txData, req, maxDataSz, common } = await buildEthSignRequest( - client, - ); + const { txData, req, maxDataSz, common } = await buildEthSignRequest(client); await question( 'Please REJECT the next request if the warning screen displays. Press enter to continue.', ); + txData.data = randomBytes(maxDataSz) req.data.data = randomBytes(maxDataSz + 1); const tx = EthTxFactory.fromTxData(txData, { common }); req.data.payload = tx.getMessageToSign(false); diff --git a/src/__test__/e2e/kv.test.ts b/src/__test__/e2e/kv.test.ts index d9c27e29..2d0d01e2 100644 --- a/src/__test__/e2e/kv.test.ts +++ b/src/__test__/e2e/kv.test.ts @@ -13,7 +13,6 @@ import { import { BTC_PURPOSE_P2PKH, ETH_COIN } from '../utils/helpers'; import { initializeClient } from '../utils/initializeClient'; -const client = initializeClient(); // Random address to test the screen with. // IMPORTANT NOTE: For Ethereum addresses you should always add the lower case variety since @@ -40,6 +39,8 @@ const ETH_REQ = { }; describe('key-value', () => { + const client = initializeClient(); + it('Should ask if the user wants to reset state', async () => { const answer = question( 'Do you want to clear all kv records and start anew? (Y/N) ', diff --git a/src/__test__/e2e/non-exportable.test.ts b/src/__test__/e2e/non-exportable.test.ts index ea907775..0b214a08 100644 --- a/src/__test__/e2e/non-exportable.test.ts +++ b/src/__test__/e2e/non-exportable.test.ts @@ -21,8 +21,9 @@ import { DEFAULT_SIGNER } from '../utils/builders'; import { getSigStr } from '../utils/helpers'; import { initializeClient } from '../utils/initializeClient'; -const client = initializeClient(); describe('Non-Exportable Seed', () => { + const client = initializeClient(); + describe('Setup', () => { it('Should ask if the user wants to test a card with a non-exportable seed', async () => { // NOTE: non-exportable seeds were deprecated from the normal setup pathway in firmware v0.12.0 diff --git a/src/__test__/e2e/signing/signing.test.ts b/src/__test__/e2e/signing/signing.test.ts index 3a66dfdc..4f5d2045 100644 --- a/src/__test__/e2e/signing/signing.test.ts +++ b/src/__test__/e2e/signing/signing.test.ts @@ -14,9 +14,9 @@ import { runEvmTests } from './evm.test'; import { runSolanaTests } from './solana.test'; import { runUnformattedTests } from './unformatted.test'; -const client = initializeClient(); - describe('Test General Signing', () => { + const client = initializeClient(); + it('Should verify firmware version.', async () => { const fwConstants = client.getFwConstants(); if (!fwConstants.genericSigning) { diff --git a/src/__test__/e2e/wallet-jobs.test.ts b/src/__test__/e2e/wallet-jobs.test.ts index ad115758..820b0b16 100644 --- a/src/__test__/e2e/wallet-jobs.test.ts +++ b/src/__test__/e2e/wallet-jobs.test.ts @@ -44,7 +44,6 @@ import { initializeClient } from '../utils/initializeClient'; import { testRequest } from '../utils/testRequest'; const id = getDeviceId(); -const client = initializeClient(); //--------------------------------------- // STATE DATA //--------------------------------------- @@ -72,25 +71,27 @@ const KNOWN_SEED = mnemonicToSeedSync(mnemonic); const wallet = bip32.fromSeed(KNOWN_SEED); describe('Test Wallet Jobs', () => { + const client = initializeClient(); - it('Should connect to a Lattice and make sure it is already paired.', async () => { - expect(id).not.toEqual(null); - await client.connect(id); + it('Should make sure client has active wallets', async () => { expect(client.isPaired).toEqual(true); const EMPTY_WALLET_UID = Buffer.alloc(32); const internalUID = client.activeWallets.internal.uid; const externalUID = client.activeWallets.external.uid; - const checkOne = !EMPTY_WALLET_UID.equals(internalUID); - const checkTwo = !EMPTY_WALLET_UID.equals(externalUID); - const checkThree = !!client.getActiveWallet(); - const checkFour = !!client.getActiveWallet()?.uid.equals(externalUID); - expect(checkOne).toEqualElseLog(true, 'Internal A90 must be enabled.'); - expect(checkTwo).toEqualElseLog( + + expect(!EMPTY_WALLET_UID.equals(internalUID)).toEqualElseLog( + true, + 'Internal A90 must be enabled.', + ); + expect(!EMPTY_WALLET_UID.equals(externalUID)).toEqualElseLog( true, 'P60 with exportable seed must be inserted.', ); - expect(checkThree).toEqualElseLog(true, 'No active wallet discovered'); - expect(checkFour).toEqualElseLog( + expect(!!client.getActiveWallet()).toEqualElseLog( + true, + 'No active wallet discovered', + ); + expect(!!client.getActiveWallet()?.uid.equals(externalUID)).toEqualElseLog( true, 'P60 should be active wallet but is not registered as it.', ); @@ -844,89 +845,89 @@ describe('Test Wallet Jobs', () => { ); }); }); -}); -//--------------------------------------- -// HELPERS -//--------------------------------------- -async function runTestCase (expectedCode: any) { - const res = await testRequest(jobReq); - //@ts-expect-error - accessing private property - const parsedRes = parseWalletJobResp(res, client.fwVersion); - expect(parsedRes.resultStatus).toEqualElseLog( - expectedCode, - getCodeMsg(parsedRes.resultStatus, expectedCode), - ); - return parsedRes; -} - -function getCurrentWalletUID () { - return copyBuffer(client.getActiveWallet()?.uid); -} - -async function runZerosTest (idx: any, numZeros: number, testPub = false) { - const w = wallet.derivePath(`${parentPathStr}/${idx}`); - const refPriv = w.privateKey; - const refPub = privateToPublic(refPriv); - for (let i = 0; i < numZeros; i++) { - if (testPub) { - expect(refPub[i]).toEqualElseLog( - 0, - `Should be ${numZeros} leading pubKey zeros but got ${i}.`, - ); - } else { - expect(refPriv[i]).toEqualElseLog( - 0, - `Should be ${numZeros} leading privKey zeros but got ${i}.`, + //--------------------------------------- + // HELPERS + //--------------------------------------- + async function runTestCase (expectedCode: any) { + const res = await testRequest(jobReq); + //@ts-expect-error - accessing private property + const parsedRes = parseWalletJobResp(res, client.fwVersion); + expect(parsedRes.resultStatus).toEqualElseLog( + expectedCode, + getCodeMsg(parsedRes.resultStatus, expectedCode), + ); + return parsedRes; + } + + function getCurrentWalletUID () { + return copyBuffer(client.getActiveWallet()?.uid); + } + + async function runZerosTest (idx: any, numZeros: number, testPub = false) { + const w = wallet.derivePath(`${parentPathStr}/${idx}`); + const refPriv = w.privateKey; + const refPub = privateToPublic(refPriv); + for (let i = 0; i < numZeros; i++) { + if (testPub) { + expect(refPub[i]).toEqualElseLog( + 0, + `Should be ${numZeros} leading pubKey zeros but got ${i}.`, + ); + } else { + expect(refPriv[i]).toEqualElseLog( + 0, + `Should be ${numZeros} leading privKey zeros but got ${i}.`, + ); + } + } + // Validate the exported address + const path = basePath; + path[path.length - 1] = idx; + const ref = `0x${privateToAddress(refPriv).toString('hex').toLowerCase()}`; + const addrs = await client.getAddresses({ startPath: path, n: 1 }) as string[]; + if (addrs[0]?.toLowerCase() !== ref) { + expect(addrs[0]?.toLowerCase()).toEqualElseLog( + ref, + 'Failed to derive correct address for known seed', ); } - } - // Validate the exported address - const path = basePath; - path[path.length - 1] = idx; - const ref = `0x${privateToAddress(refPriv).toString('hex').toLowerCase()}`; - const addrs = await client.getAddresses({ startPath: path, n: 1 }) as string[]; - if (addrs[0]?.toLowerCase() !== ref) { - expect(addrs[0]?.toLowerCase()).toEqualElseLog( - ref, - 'Failed to derive correct address for known seed', + // Validate the signer coming back from the sign request + const tx = EthTxFactory.fromTxData( + { + type: 1, + gasPrice: 1200000000, + nonce: 0, + gasLimit: 50000, + to: '0xe242e54155b1abc71fc118065270cecaaf8b7768', + value: 1000000000000, + data: '0x17e914679b7e160613be4f8c2d3203d236286d74eb9192f6d6f71b9118a42bb033ccd8e8', + }, + { + common: new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + }), + }, ); + const txReq = { + data: { + signerPath: path, + curveType: Constants.SIGNING.CURVES.SECP256K1, + hashType: Constants.SIGNING.HASHES.KECCAK256, + encodingType: Constants.SIGNING.ENCODINGS.EVM, + payload: tx.getMessageToSign(false), + }, + }; + const resp: any = await client.sign(txReq); + // Make sure the exported signer matches expected + expect(resp.pubkey.slice(1).toString('hex')).toEqualElseLog( + refPub.toString('hex'), + 'Incorrect signer', + ); + // Make sure we can recover the same signer from the sig. + // `getV` will only return non-null if it can successfully + // ecrecover a pubkey that matches the one provided. + expect(getV(tx, resp)).not.toEqualElseLog(null, 'Incorrect signer'); } - // Validate the signer coming back from the sign request - const tx = EthTxFactory.fromTxData( - { - type: 1, - gasPrice: 1200000000, - nonce: 0, - gasLimit: 50000, - to: '0xe242e54155b1abc71fc118065270cecaaf8b7768', - value: 1000000000000, - data: '0x17e914679b7e160613be4f8c2d3203d236286d74eb9192f6d6f71b9118a42bb033ccd8e8', - }, - { - common: new Common({ - chain: Chain.Mainnet, - hardfork: Hardfork.London, - }), - }, - ); - const txReq = { - data: { - signerPath: path, - curveType: Constants.SIGNING.CURVES.SECP256K1, - hashType: Constants.SIGNING.HASHES.KECCAK256, - encodingType: Constants.SIGNING.ENCODINGS.EVM, - payload: tx.getMessageToSign(false), - }, - }; - const resp: any = await client.sign(txReq); - // Make sure the exported signer matches expected - expect(resp.pubkey.slice(1).toString('hex')).toEqualElseLog( - refPub.toString('hex'), - 'Incorrect signer', - ); - // Make sure we can recover the same signer from the sig. - // `getV` will only return non-null if it can successfully - // ecrecover a pubkey that matches the one provided. - expect(getV(tx, resp)).not.toEqualElseLog(null, 'Incorrect signer'); -} +}); diff --git a/src/__test__/unit/__snapshots__/validators.test.ts.snap b/src/__test__/unit/__snapshots__/validators.test.ts.snap index 168c4b55..664f91fc 100644 --- a/src/__test__/unit/__snapshots__/validators.test.ts.snap +++ b/src/__test__/unit/__snapshots__/validators.test.ts.snap @@ -83,7 +83,6 @@ exports[`validators > fetchActiveWallet > should successfully validate 1`] = ` exports[`validators > getAddresses > should successfully validate 1`] = ` { - "flag": 1, "fwVersion": { "data": [ 0, diff --git a/src/__test__/utils/builders.ts b/src/__test__/utils/builders.ts index 4594d115..59a56e00 100644 --- a/src/__test__/utils/builders.ts +++ b/src/__test__/utils/builders.ts @@ -183,7 +183,7 @@ export const buildTx = (data = '0xdeadbeef') => { }; export const buildEthSignRequest = async ( - client: Client + client: Client, txDataOverrides?: any ): Promise => { if (client.getFwVersion()?.major === 0 && client.getFwVersion()?.minor < 15) { console.warn('Please update firmware. Skipping ETH signing tests.'); @@ -205,6 +205,7 @@ export const buildEthSignRequest = async ( to: '0xe242e54155b1abc71fc118065270cecaaf8b7768', value: 1000000000000, data: '0x17e914679b7e160613be4f8c2d3203d236286d74eb9192f6d6f71b9118a42bb033ccd8e8', + ...txDataOverrides }; const tx = EthTxFactory.fromTxData(txData, { common }); const req = { diff --git a/src/__test__/utils/initializeClient.ts b/src/__test__/utils/initializeClient.ts index 3a6dd076..fdfce487 100644 --- a/src/__test__/utils/initializeClient.ts +++ b/src/__test__/utils/initializeClient.ts @@ -23,7 +23,7 @@ export const initializeClient = () => { if (!isPaired) { expect(client.isPaired).toEqual(false); const secret = question('Please enter the pairing secret: '); - await client.pair(secret); + await client.pair(secret.toUpperCase()); expect(!!client.getActiveWallet()).toEqual(true); } expect(client.isPaired).toEqual(true); diff --git a/src/__test__/utils/setup.ts b/src/__test__/utils/setup.ts index 72ff0d1c..2ca583b7 100644 --- a/src/__test__/utils/setup.ts +++ b/src/__test__/utils/setup.ts @@ -5,4 +5,4 @@ expect.extend({ message: () => message ? message : `Expected ${received} to equal ${expected}`, }; }, -}); +}); \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index 866e120c..fc4cc487 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,7 +14,8 @@ import { removeKvRecords, sign } from './functions/index'; -import { retryWrapper } from './shared/functions'; +import { buildRetryWrapper } from './shared/functions'; +import { validateEphemeralPub } from './shared/validators'; import { getP256KeyPair, getP256KeyPairFromPub, randomBytes } from './util'; /** @@ -25,9 +26,9 @@ export class Client { public isPaired: boolean; /** The time to wait for a response before cancelling. */ public timeout: number; - /** The remote url to which the SDK sends requests. */ + /** The base of the remote url to which the SDK sends requests. */ public baseUrl: string; - /** @internal */ + /** @internal The `baseUrl` plus the `deviceId`. Set in {@link connect} when it completes successfully. */ public url?: string; /** `name` is a human readable string associated with this app on the Lattice */ private name: string; @@ -44,6 +45,8 @@ export class Client { private deviceId: string | null; /** Information about the current wallet. Should be null unless we know a wallet is present */ public activeWallets: ActiveWallets; + /** A wrapper function for handling retries and injecting the {@link Client} class */ + private retryWrapper: (fn: any, params?: any) => Promise; /** * @param params - Parameters are passed as an object. @@ -82,6 +85,7 @@ export class Client { this.skipRetryOnWrongWallet = skipRetryOnWrongWallet || false; this.privKey = privKey || randomBytes(32); this.key = getP256KeyPair(this.privKey); + this.retryWrapper = buildRetryWrapper(this, this.retryCount); /** The user may pass in state data to rehydrate a session that was previously cached */ if (stateData) { @@ -109,9 +113,8 @@ export class Client { /** @internal */ public set ephemeralPub (ephemeralPub: KeyPair) { - if (ephemeralPub) { - this._ephemeralPub = ephemeralPub; - } + validateEphemeralPub(ephemeralPub) + this._ephemeralPub = ephemeralPub; } /** @@ -120,15 +123,7 @@ export class Client { * @category Lattice */ public async connect (deviceId: string) { - return retryWrapper({ - fn: connect, - params: { - id: deviceId, - client: this, - }, - retries: 3, - client: this - }) + return this.retryWrapper(connect, { id: deviceId }) } /** @@ -138,15 +133,7 @@ export class Client { * @category Lattice */ public async pair (pairingSecret: string) { - return retryWrapper({ - fn: pair, - params: { - pairingSecret, - client: this, - }, - retries: 3, - client: this - }) + return this.retryWrapper(pair, { pairingSecret }) } /** @@ -158,17 +145,7 @@ export class Client { n = 1, flag = 0, }: GetAddressesRequestParams): Promise { - return retryWrapper({ - fn: getAddresses, - params: { - startPath, - n, - flag, - client: this - }, - retries: 3, - client: this - }) + return this.retryWrapper(getAddresses, { startPath, n, flag }) } /** @@ -180,34 +157,15 @@ export class Client { currency, cachedData, nextCode, - retries = 3 }: SignRequestParams): Promise { - return retryWrapper({ - fn: sign, - params: { - data, - currency, - cachedData, - nextCode, - client: this, - }, - retries, - client: this - }) + return this.retryWrapper(sign, { data, currency, cachedData, nextCode }) } /** * Fetch the active wallet in the Lattice. */ public async fetchActiveWallet (): Promise { - return retryWrapper({ - fn: fetchActiveWallet, - params: { - client: this, - }, - retries: 3, - client: this - }) + return this.retryWrapper(fetchActiveWallet) } /** @@ -219,17 +177,7 @@ export class Client { records, caseSensitive = false, }: AddKvRecordsRequestParams): Promise { - return retryWrapper({ - fn: addKvRecords, - params: { - type, - records, - caseSensitive, - client: this, - }, - retries: 3, - client: this - }) + return this.retryWrapper(addKvRecords, { type, records, caseSensitive, }) } /** @@ -241,17 +189,7 @@ export class Client { n = 1, start = 0, }: GetKvRecordsRequestParams): Promise { - return retryWrapper({ - fn: getKvRecords, - params: { - type, - n, - start, - client: this, - }, - retries: 3, - client: this - }) + return this.retryWrapper(getKvRecords, { type, n, start, }) } /** @@ -262,16 +200,7 @@ export class Client { type = 0, ids = [], }: RemoveKvRecordsRequestParams): Promise { - return retryWrapper({ - fn: removeKvRecords, - params: { - type, - ids, - client: this, - }, - retries: 3, - client: this - }) + return this.retryWrapper(removeKvRecords, { type, ids, }) } /** Get the active wallet */ @@ -421,6 +350,7 @@ export class Client { this.key = getP256KeyPair(this.privKey); this.retryCount = unpacked.retryCount; this.timeout = unpacked.timeout; + this.retryWrapper = buildRetryWrapper(this, this.retryCount); } catch (err) { console.warn('Could not apply state data.'); } diff --git a/src/functions/fetchActiveWallet.ts b/src/functions/fetchActiveWallet.ts index ab444cf1..e585809d 100644 --- a/src/functions/fetchActiveWallet.ts +++ b/src/functions/fetchActiveWallet.ts @@ -11,8 +11,11 @@ import { } from '../shared/validators'; /** - * Fetch the active wallet in the device. - * @returns callback with an error or null + * Fetch the active wallet in the device. + * + * The Lattice has two wallet interfaces: internal and external. If a SafeCard is inserted and + * unlocked, the external interface is considered "active" and this will return its {@link Wallet} + * data. Otherwise it will return the info for the internal Lattice wallet. */ export async function fetchActiveWallet ({ client, diff --git a/src/functions/getAddresses.ts b/src/functions/getAddresses.ts index 9bd98060..c3aea110 100644 --- a/src/functions/getAddresses.ts +++ b/src/functions/getAddresses.ts @@ -19,22 +19,22 @@ import { import { isValidAssetPath } from '../util'; /** - * `getAddresses` takes a starting path and a number to get the addresses associated with the - * active wallet. + * `getAddresses` takes a starting path and a number to get the addresses or public keys associated + * with the active wallet. * @category Lattice - * @returns An array of addresses. + * @returns An array of addresses or public keys. */ export async function getAddresses ({ startPath, n, - flag: _flag, + flag, client, }: GetAddressesRequestFunctionParams): Promise { - const { url, fwVersion, wallet, sharedSecret, flag } = + const { url, fwVersion, wallet, sharedSecret } = validateGetAddressesRequest({ startPath, n, - flag: _flag, + flag, url: client.url, fwVersion: client.fwVersion, wallet: client.getActiveWallet(), @@ -79,7 +79,7 @@ export const validateGetAddressesRequest = ({ }: ValidateGetAddressesRequestParams) => { validateStartPath(startPath); validateNAddresses(n); - const validFlag = validateIsUInt4(flag); + validateIsUInt4(flag); const validUrl = validateUrl(url); const validFwVersion = validateFwVersion(fwVersion); const validWallet = validateWallet(wallet); @@ -90,7 +90,6 @@ export const validateGetAddressesRequest = ({ fwVersion: validFwVersion, wallet: validWallet, sharedSecret: validSharedSecret, - flag: validFlag, }; }; @@ -175,9 +174,8 @@ export const requestGetAddresses = async (payload: Buffer, url: string) => { }; /** - * @category Device Response * @internal - * @return an array of address strings + * @return an array of address strings or pubkey buffers */ export const decodeGetAddresses = (data: any, flag: number): Buffer[] => { let off = 65; // Skip 65 byte pubkey prefix diff --git a/src/shared/errors.ts b/src/shared/errors.ts index ad54c426..d131080b 100644 --- a/src/shared/errors.ts +++ b/src/shared/errors.ts @@ -3,8 +3,7 @@ import { responseMsgs } from '../constants'; const buildLatticeResponseErrorMessage = ({ responseCode, errorMessage }) => { const msg: string[] = []; if (responseCode) { - msg.push(`Response Code: ${responseCode}`); - msg.push(`Message: ${responseMsgs[responseCode]}`); + msg.push(`${responseMsgs[responseCode]}`); } if (errorMessage) { msg.push('Error Message: '); diff --git a/src/shared/functions.ts b/src/shared/functions.ts index a5533a7e..6e11cf16 100644 --- a/src/shared/functions.ts +++ b/src/shared/functions.ts @@ -1,5 +1,6 @@ import { sha256 } from 'hash.js/lib/hash/sha'; import superagent from 'superagent'; +import { Client } from '..'; import bitcoin from '../bitcoin'; import { deviceCodes, @@ -7,7 +8,6 @@ import { ENC_MSG_LEN, EXTERNAL, REQUEST_TYPE_BYTE, - responseMsgs, VERSION_BYTE, } from '../constants'; import ethereum from '../ethereum'; @@ -200,7 +200,7 @@ export const request = async ({ } return data; - }) + }); }; /** @@ -211,36 +211,51 @@ function sleep (ms) { } /** - * `retryWrapper()` retries a function call if the error message or response code is present and the - * number of retries is greater than 0. + * Takes a function and a set of parameters, and returns a function that will retry the original + * function with the given parameters a number of times + * + * @param client - a {@link Client} instance that is passed to the {@link retryWrapper} + * @param retries - the number of times to retry the function before giving up + * @returns a {@link retryWrapper} function for handing retry logic + */ +export const buildRetryWrapper = (client: Client, retries: number) => { + return (fn, params?) => + retryWrapper({ + fn, + params: { ...params, client }, + retries, + client, + }); +}; + +/** + * Retries a function call if the error message or response code is present and the number of + * retries is greater than 0. * * @param fn - The function to retry * @param params - The parameters to pass to the function * @param retries - The number of times to retry the function - * @param client - The Client to use for side-effects + * @param client - The {@link Client} to use for side-effects */ -export const retryWrapper = async ({ - fn, - params, - retries, - client, -}) => { +export const retryWrapper = async ({ fn, params, retries, client }) => { return fn({ ...params }).catch(async (err) => { + /** `string` returned from the Lattice if there's an error */ const errorMessage = err.errorMessage; + /** `number` returned from the Lattice if there's an error */ const responseCode = err.responseCode; if ((errorMessage || responseCode) && retries) { - if (isDeviceBusy(responseCode)) { await sleep(3000); - } - - if (isWrongWallet(responseCode) && !client.skipRetryOnWrongWallet) { + } else if ( + isWrongWallet(responseCode) && + !client.skipRetryOnWrongWallet + ) { await client.fetchActiveWallet(); - } - - if (isInvalidEphemeralId(responseCode)) { + } else if (isInvalidEphemeralId(responseCode)) { await client.connect(client.deviceId); + } else { + throw err; } return retryWrapper({ @@ -250,6 +265,7 @@ export const retryWrapper = async ({ client, }); } + throw err; }); }; diff --git a/src/shared/validators.ts b/src/shared/validators.ts index 2c729443..5979b450 100644 --- a/src/shared/validators.ts +++ b/src/shared/validators.ts @@ -17,7 +17,6 @@ export const validateIsUInt4 = (n?: number) => { }; export const validateNAddresses = (n: number) => { - validateIsUInt4(n); if (n > MAX_ADDR) throw new Error(`You may only request ${MAX_ADDR} addresses at once.`); }; @@ -84,7 +83,7 @@ export const validateFwConstants = (fwConstants?: FirmwareConstants) => { return fwConstants; }; export const validateFwVersion = (fwVersion?: Buffer) => { - if (!fwVersion) { + if (!fwVersion || fwVersion.byteLength > 4) { throw new Error('Firmware version does not exist. Please reconnect.'); } return fwVersion;