Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mobile keyring bridge #225

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
42e8291
feat: add mobile keyring bridge
stanleyyconsensys Mar 28, 2024
0cecf35
chore: update version number
stanleyyconsensys Mar 28, 2024
7375fa0
feat: upgrade @ethereumjs/tx library to match the same version used i…
dawnseeker8 Mar 28, 2024
a830897
feat: upgrade ledgerHQ library to latest to resolve vulnerablity repo…
dawnseeker8 Mar 28, 2024
d6b8138
Feat/add mobile keyring bridge flat version (#3)
dawnseeker8 Apr 26, 2024
ef871ab
feat: add unit tests coverage
dawnseeker8 Apr 29, 2024
366eeb7
Merge branch 'main' into feat/add-mobile-keyring-bridge
dawnseeker8 Apr 29, 2024
549d558
feat: fix lint error in test file.
dawnseeker8 Apr 29, 2024
dde4f28
feat: Remove unit tests which failed.
dawnseeker8 Apr 29, 2024
2651a04
feat: Add extra test to cover more lines in ledger-keyring.ts
dawnseeker8 Apr 29, 2024
2d24e3c
feat: Add yarn lint:fix
dawnseeker8 Apr 29, 2024
eee90ec
feat: Increase the threshold coverage to pass the pipeline.
dawnseeker8 Apr 29, 2024
4b6a5da
feat: revert the version of package..
dawnseeker8 Apr 29, 2024
c106d7b
Update src/ledger-transport-middleware.ts
dawnseeker8 Jun 14, 2024
d25cec3
Update src/ledger-mobile-bridge.test.ts
dawnseeker8 Jun 14, 2024
1ba9939
Update src/ledger-mobile-bridge.test.ts
dawnseeker8 Jun 14, 2024
d3fa752
Update src/ledger-mobile-bridge.test.ts
dawnseeker8 Jun 14, 2024
70518bd
Apply suggestions from code review
dawnseeker8 Jun 14, 2024
4d68c85
feat: remove the comment code in `prepack.sh`
dawnseeker8 Jun 14, 2024
84d2805
feat: change the jsDoc for deviceSignMessage method.
dawnseeker8 Jun 14, 2024
3e326de
feat: fix the lint issue.
dawnseeker8 Jun 14, 2024
af27ce6
Apply suggestions from code review
dawnseeker8 Jun 20, 2024
aed3826
Apply suggestions from code review
dawnseeker8 Jun 20, 2024
d5f1812
feat: change setAccountToUnlock method signature to enforce index is …
dawnseeker8 Jun 20, 2024
062e961
feat: change setAccountToUnlock method signature to enforce index is …
dawnseeker8 Jun 20, 2024
54419ad
feat: commit the new coverageThreshold to pass the test.
dawnseeker8 Jun 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 69.76,
functions: 88.73,
lines: 81.93,
statements: 81.84,
branches: 87.94,
functions: 95.91,
lines: 91.13,
statements: 91.22,
},
},

Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@
},
"dependencies": {
"@ethereumjs/rlp": "^4.0.0",
"@ethereumjs/tx": "^4.1.1",
"@ethereumjs/tx": "^4.2.0",
"@ethereumjs/util": "^8.0.0",
"@metamask/eth-sig-util": "^7.0.0",
"@ledgerhq/hw-app-eth": "6.26.1",
"@metamask/eth-sig-util": "^7.0.1",
"hdkey": "^2.1.0"
},
"devDependencies": {
"@ethereumjs/common": "^3.1.1",
"@ethereumjs/common": "^3.2.0",
"@lavamoat/allow-scripts": "^2.5.1",
"@ledgerhq/hw-app-eth": "^6.32.0",
"@ledgerhq/hw-transport": "^6.24.1",
"@ledgerhq/types-cryptoassets": "^7.6.0",
"@ledgerhq/types-devices": "^6.22.4",
"@metamask/auto-changelog": "^3.1.0",
Expand Down Expand Up @@ -95,7 +96,8 @@
"@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false,
"ethereumjs-tx>ethereumjs-util>keccak": false,
"ethereumjs-tx>ethereumjs-util>secp256k1": false,
"hdkey>secp256k1": false
"hdkey>secp256k1": false,
"ethereumjs-tx>ethereumjs-util>ethereum-cryptography>keccak": false
}
}
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export * from './ledger-keyring';
export * from './ledger-iframe-bridge';
export * from './ledger-mobile-bridge';
export * from './ledger-bridge';
export * from './ledger-transport-middleware';
export * from './type';
export * from './ledger-hw-app';
7 changes: 3 additions & 4 deletions src/ledger-bridge.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type LedgerHwAppEth from '@ledgerhq/hw-app-eth';
import type Transport from '@ledgerhq/hw-transport';

export type GetPublicKeyParams = { hdPath: string };
export type GetPublicKeyResponse = Awaited<
ReturnType<LedgerHwAppEth['getAddress']>
> & {
chainCode: string;
};
>;

export type LedgerSignTransactionParams = { hdPath: string; tx: string };
export type LedgerSignTransactionResponse = Awaited<
Expand Down Expand Up @@ -49,7 +48,7 @@ export type LedgerBridge<T extends LedgerBridgeOptions> = {

attemptMakeApp(): Promise<boolean>;

updateTransportMethod(transportType: string): Promise<boolean>;
updateTransportMethod(transportType: string | Transport): Promise<boolean>;

getPublicKey(params: GetPublicKeyParams): Promise<GetPublicKeyResponse>;

Expand Down
128 changes: 128 additions & 0 deletions src/ledger-hw-app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Transport from '@ledgerhq/hw-transport';

import { MetaMaskLedgerHwAppEth } from './ledger-hw-app';

const DEVICE_ID = 'DEVICE_ID';

const mockTransport = {
deviceModel: {
id: DEVICE_ID,
},
send: jest.fn(),
close: jest.fn(),
decorateAppAPIMethods: jest.fn(),
};

describe('MetaMaskLedgerHwAppEth', function () {
afterEach(function () {
jest.clearAllMocks();
});

describe('openEthApp', function () {
it('sends "open ETH app" command correctly', async function () {
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(Buffer.alloc(1)));

await ethApp.openEthApp();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(
0xe0,
0xd8,
0x00,
0x00,
Buffer.from('Ethereum', 'ascii'),
);
});
});

describe('closeApps', function () {
it('sends "closeApp" command correctly', async function () {
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(Buffer.alloc(1)));

await ethApp.closeApps();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0xa7, 0x00, 0x00);
});
});

describe('getAppNameAndVersion', function () {
it('gets appName and appVersion correctly', async function () {
const appNameBuf = Buffer.alloc(7, 'appName', 'ascii');
const verionBuf = Buffer.alloc(10, 'appVersion', 'ascii');
const buffer = Buffer.alloc(20);
buffer[0] = 1;
buffer[1] = appNameBuf.length;
let j = 2;
for (let i = 0; i < appNameBuf.length; i++, j++) {
buffer[j] = appNameBuf[i] ?? 0;
}
buffer[j] = verionBuf.length;
j += 1;
for (let i = 0; i < verionBuf.length; i++, j++) {
buffer[j] = verionBuf[i] ?? 0;
}

const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(buffer));

const result = await ethApp.getAppNameAndVersion();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0x01, 0x00, 0x00);
expect(result).toStrictEqual({
appName: 'appName',
version: 'appVersion',
});
});

it('does not throw an error when the result length is less than expected', async function () {
const buffer = Buffer.alloc(1);
buffer[0] = 1;
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);
const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(buffer));

const result = await ethApp.getAppNameAndVersion();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0x01, 0x00, 0x00);
expect(result).toStrictEqual({
appName: '',
version: '',
});
});

it('throws an error when first byte is not 1', async function () {
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(Buffer.alloc(1)));

await expect(ethApp.getAppNameAndVersion()).rejects.toThrow(
'Incorrect format return from getAppNameAndVersion.',
);
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0x01, 0x00, 0x00);
});
});
});
77 changes: 77 additions & 0 deletions src/ledger-hw-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import LedgerHwAppEth from '@ledgerhq/hw-app-eth';
// eslint-disable-next-line import/no-nodejs-modules
import { Buffer } from 'buffer';

import { GetAppNameAndVersionResponse } from './type';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface MetaMaskLedgerHwAppEth extends LedgerHwAppEth {
openEthApp(): void;
closeApps(): void;
getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse>;
}

export class MetaMaskLedgerHwAppEth
extends LedgerHwAppEth
implements MetaMaskLedgerHwAppEth
{
readonly mainAppName = 'BOLOS';

readonly ethAppName = 'Ethereum';

readonly transportEncoding = 'ascii';

/**
* Method to open ethereum application on ledger device.
*
*/
async openEthApp(): Promise<void> {
await this.transport.send(
0xe0,
0xd8,
0x00,
0x00,
Buffer.from(this.ethAppName, this.transportEncoding),
);
}

/**
* Method to close all running application on ledger device.
*
*/
async closeApps(): Promise<void> {
await this.transport.send(0xb0, 0xa7, 0x00, 0x00);
}

/**
* Method to retrieve the name and version of the running application in ledger device.
*
* @returns An object contains appName and version.
*/
async getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse> {
const response = await this.transport.send(0xb0, 0x01, 0x00, 0x00);
if (response[0] !== 1) {
throw new Error('Incorrect format return from getAppNameAndVersion.');
}

let i = 1;
const nameLength = response[i] ?? 0;
i += 1;

const appName = response
.slice(i, (i += nameLength))
.toString(this.transportEncoding);

const versionLength = response[i] ?? 0;
i += 1;

const version = response
.slice(i, (i += versionLength))
.toString(this.transportEncoding);

return {
appName,
version,
};
}
}
Loading
Loading