Skip to content

Commit

Permalink
Adds back retry logic using a wrapper function
Browse files Browse the repository at this point in the history
  • Loading branch information
douglance committed Jun 23, 2022
1 parent 9acb48a commit 0926aa3
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 71 deletions.
14 changes: 7 additions & 7 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"envFile": "${workspaceFolder}/.env",
"name": "E2E Tests - All",
"request": "launch",
"runtimeArgs": ["run-script", "test-e2e"],
"runtimeArgs": ["run-script", "e2e"],
"runtimeExecutable": "npm",
"type": "node"
},
Expand All @@ -37,7 +37,7 @@
"envFile": "${workspaceFolder}/.env",
"name": "E2E Tests - General",
"request": "launch",
"runtimeArgs": ["run-script", "test-gen"],
"runtimeArgs": ["run-script", "e2e-gen"],
"runtimeExecutable": "npm",
"type": "node"
},
Expand All @@ -47,7 +47,7 @@
"envFile": "${workspaceFolder}/.env",
"name": "E2E Tests - Bitcoin",
"request": "launch",
"runtimeArgs": ["run-script", "test-btc"],
"runtimeArgs": ["run-script", "e2e-btc"],
"runtimeExecutable": "npm",
"type": "node"
},
Expand All @@ -57,7 +57,7 @@
"envFile": "${workspaceFolder}/.env",
"name": "E2E Tests - Signing",
"request": "launch",
"runtimeArgs": ["run-script", "test-sign"],
"runtimeArgs": ["run-script", "e2e-sign"],
"runtimeExecutable": "npm",
"type": "node"
},
Expand All @@ -67,7 +67,7 @@
"envFile": "${workspaceFolder}/.env",
"name": "E2E Tests - Key-Value",
"request": "launch",
"runtimeArgs": ["run-script", "test-kv"],
"runtimeArgs": ["run-script", "e2e-kv"],
"runtimeExecutable": "npm",
"type": "node"
},
Expand All @@ -77,7 +77,7 @@
"envFile": "${workspaceFolder}/.env",
"name": "E2E Tests - Non-Exportable Card",
"request": "launch",
"runtimeArgs": ["run-script", "test-ne"],
"runtimeArgs": ["run-script", "e2e-ne"],
"runtimeExecutable": "npm",
"type": "node"
},
Expand All @@ -87,7 +87,7 @@
"envFile": "${workspaceFolder}/.env",
"name": "E2E Tests - Wallet Jobs",
"request": "launch",
"runtimeArgs": ["run-script", "test-wj"],
"runtimeArgs": ["run-script", "e2e-wj"],
"runtimeExecutable": "npm",
"type": "node"
}
Expand Down
34 changes: 22 additions & 12 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { BASE_URL, DEFAULT_ACTIVE_WALLETS, EMPTY_WALLET_UID, getFwVersionConst } from './constants';
import {
BASE_URL,
DEFAULT_ACTIVE_WALLETS,
EMPTY_WALLET_UID,
getFwVersionConst
} from './constants';
import {
addKvRecords,
connect,
Expand All @@ -7,9 +12,10 @@ import {
getKvRecords,
pair,
removeKvRecords,
sign,
sign
} from './functions/index';
import { randomBytes, getP256KeyPair, getP256KeyPairFromPub } from './util';
import { retryWrapper } from './shared/functions';
import { getP256KeyPair, getP256KeyPairFromPub, randomBytes } from './util';

/**
* `Client` is a class-based interface for managing a Lattice device.
Expand Down Expand Up @@ -82,13 +88,11 @@ export class Client {
}

public get ephemeralPub () {
// console.debug('Getting ephemPub', this._ephemeralPub)
return this._ephemeralPub;
}

public set ephemeralPub (ephemeralPub: KeyPair) {
if (ephemeralPub) {
// console.debug('Setting ephemPub', ephemeralPub)
this._ephemeralPub = ephemeralPub;
}
}
Expand Down Expand Up @@ -137,14 +141,20 @@ export class Client {
currency,
cachedData,
nextCode,
retries = 3
}: SignRequestParams): Promise<SignData> {
return sign({
data,
currency,
cachedData,
nextCode,
client: this,
});
return retryWrapper({
fn: sign,
params: {
data,
currency,
cachedData,
nextCode,
client: this,
},
retries,
client: this
})
}

/**
Expand Down
27 changes: 27 additions & 0 deletions src/shared/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { responseMsgs } from '../constants';

const buildLatticeResponseErrorMessage = ({ responseCode, errorMessage }) => {
const msg: string[] = [];
if (responseCode) {
msg.push(`Response Code: ${responseCode}`);
msg.push(`Message: ${responseMsgs[responseCode]}`);
}
if (errorMessage) {
msg.push('Error Message: ');
msg.push(errorMessage);
}
return msg.join('\n');
};

export class LatticeResponseError extends Error {
constructor(
public responseCode: number,
public errorMessage: string,
) {
const message = buildLatticeResponseErrorMessage({ responseCode, errorMessage });
super(message);
this.name = 'LatticeResponseError';
this.responseCode = responseCode;
this.errorMessage = errorMessage;
}
}
110 changes: 69 additions & 41 deletions src/shared/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ENC_MSG_LEN,
EXTERNAL,
REQUEST_TYPE_BYTE,
responseMsgs,
VERSION_BYTE,
} from '../constants';
import ethereum from '../ethereum';
Expand All @@ -19,7 +20,8 @@ import {
parseLattice1Response,
randomBytes,
} from '../util';
import { shouldUseEVMLegacyConverter } from './predicates';
import { LatticeResponseError } from './errors';
import { isDeviceBusy, isInvalidEphemeralId, isWrongWallet, shouldUseEVMLegacyConverter } from './predicates';
import {
validateChecksum,
validateRequestError,
Expand Down Expand Up @@ -118,15 +120,15 @@ export const buildTransaction = ({
if (currency === 'ETH' && shouldUseEVMLegacyConverter(fwConstants)) {
console.log(
'Using the legacy ETH signing path. This will soon be deprecated. ' +
'Please switch to general signing request.',
'Please switch to general signing request.',
);
let payload;
try {
payload = ethereum.ethConvertLegacyToGenericReq(data);
} catch (err) {
throw new Error(
'Could not convert legacy request. Please switch to a general signing ' +
'request. See gridplus-sdk docs for more information.',
'request. See gridplus-sdk docs for more information.',
);
}
data = {
Expand Down Expand Up @@ -164,54 +166,78 @@ export const buildTransaction = ({
};
};

const defaultTimeout = {
//TODO Test
response: 50000, // Wait 5 seconds for the server to respond
deadline: 600000, // but allow 1 minute for the user to accept
};

interface LatticeRequest extends superagent.Response {
body: {
status: number;
message: string;
};
}

export const request = async ({
url,
payload,
timeout = defaultTimeout,
retries = 3,
timeout = 60000,
}: RequestParams) => {
const res: LatticeRequest | void = await superagent
return superagent
.post(url)
.timeout(timeout)
.retry(retries)
.send({ data: payload })
.catch((err: LatticeError) => {
validateRequestError(err);
.then(async (response) => {
// Handle formatting or generic HTTP errors
if (!response.body || !response.body.message) {
throw new Error(`Invalid response: ${response.body.message}`);
} else if (response.body.status !== 200) {
throw new Error(
`Error code ${response.body.status}: ${response.body.message}`,
);
}

const { data, errorMessage, responseCode } = parseLattice1Response(
response.body.message,
);

if ((errorMessage || responseCode)) {
throw new LatticeResponseError(responseCode, errorMessage);
}

return data;
});
};

// Handle formatting or generic HTTP errors
if (!res || !res.body) {
throw new Error(`Invalid response: ${res}`);
} else if (res.body.status !== 200) {
throw new Error(`Error code ${res.body.status}: ${res.body.message}`);
}
/**
* `sleep()` returns a Promise that resolves after a given number of milliseconds.
*/
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// TODO: Update ResponseCode handling const { responseCode, err, data } = parseLattice1Response(res.body.message);
const { err, data } = parseLattice1Response(res.body.message);
/**
* `retryWrapper()` 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
*/
export const retryWrapper = async ({ fn, params, retries, client }) => {
return fn({ ...params }).catch(async err => {
const errorMessage = err.errorMessage
const responseCode = err.responseCode

if (err) {
//TODO: Handle response codes
throw new Error(
// `Request Failed\nResponse Code: ${responseCode}\nError Message: ${err}`,
`${err}`,
);
}
if ((errorMessage || responseCode) && retries) {

return data;
};
if (isDeviceBusy(responseCode)) {
await sleep(3000);
return retryWrapper({ fn, params, retries: retries - 1, client });
}

if (isWrongWallet(responseCode)) {
await client.fetchActiveWallet();
return retryWrapper({ fn, params, retries: retries - 1, client });
}

if (isInvalidEphemeralId(responseCode)) {
await client.connect(client.deviceId);
return retryWrapper({ fn, params, retries: retries - 1, client });
}
}
throw err;
})
}

/**
* All encrypted responses must be decrypted with the previous shared secret. Per specification,
Expand All @@ -221,11 +247,13 @@ export const request = async ({
* @internal
*/
export const decryptResponse = (
encryptedResponse: Buffer, //TODO: add type for responses
encryptedResponse: Buffer,
length: number,
sharedSecret: Buffer,
): DecryptedResponse => {
// Decrypt response
if (!encryptedResponse || encryptedResponse.length < length) {
return { decryptedData: null, newEphemeralPub: null };
}
const encData = encryptedResponse.slice(0, ENC_MSG_LEN);
const decryptedData = aes256_decrypt(encData, sharedSecret);

Expand Down
5 changes: 1 addition & 4 deletions src/types/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,7 @@ interface EncryptRequestParams {
interface RequestParams {
url: string;
payload: any; //TODO Fix this any
timeout?: {
response: number;
deadline: number;
};
timeout?: number;
retries?: number;
}

Expand Down
1 change: 1 addition & 0 deletions src/types/sign.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ interface SignRequestParams {
currency?: Currency;
cachedData?: any;
nextCode?: Buffer;
retries?: number;
}

interface SignRequestFunctionParams extends SignRequestParams {
Expand Down
Loading

0 comments on commit 0926aa3

Please sign in to comment.