Skip to content

Commit

Permalink
GridPlus implementation (#22)
Browse files Browse the repository at this point in the history
* Start experimenting with GridPlus

* Work on pairing process

* Fix address scanning

* Hexlify signing params

* Fixes to signature logic

* Add basic SDK typing and other improvements

* Add function to get credentials for saving

* Run formatting

* Fix issue with empty buffer being returned for v

* Fix message signing

* Improve error handling

* Move condition

* Add some tests and mock

* Improve some typing and add more tests

* Attempt to simplify client management

* Add more tests

* Add timeout to popup

* Add test for timeout

* Revert popup timeout and detect closing the popup instead

* Fix test

* Fix some PR comments

* Fix more PR comments

* Simplify slightly

* Add missing test

* Small PR comments

* Bump dep and version
  • Loading branch information
FrederikBolding authored Nov 15, 2021
1 parent 6124175 commit a6bff71
Show file tree
Hide file tree
Showing 15 changed files with 1,107 additions and 9 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mycrypto/wallets",
"version": "1.3.5",
"version": "1.4.0",
"description": "Wallet abstractions to be used throughout the MyCrypto product suite.",
"repository": "MyCryptoHQ/wallets",
"author": "MyCrypto",
Expand Down Expand Up @@ -48,6 +48,7 @@
"@ledgerhq/hw-transport-webhid": "6.4.1",
"@ledgerhq/hw-transport-webusb": "6.6.0",
"ethereumjs-wallet": "1.0.1",
"gridplus-sdk": "0.9.1",
"superstruct": "0.15.1",
"trezor-connect": "8.2.0"
},
Expand Down
2 changes: 2 additions & 0 deletions src/dpaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,5 @@ export const LEDGER_DERIVATION_PATHS: DerivationPath[] = [
export const TREZOR_DERIVATION_PATHS: DerivationPath[] = [
...ALL_DERIVATION_PATHS.filter((path) => !path.isHardened)
];

export const GRIDPLUS_DERIVATION_PATHS: DerivationPath[] = [DEFAULT_ETH];
101 changes: 101 additions & 0 deletions src/implementations/deterministic/__mocks__/gridplus-sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { arrayify, splitSignature } from '@ethersproject/bytes';
import { HDNode } from '@ethersproject/hdnode';
import type { Transaction } from '@ethersproject/transactions';
import { parse } from '@ethersproject/transactions';
import { Wallet } from '@ethersproject/wallet';
import type {
AddressesOpts,
SignMessageOpts,
SignOpts,
SignResult,
SignTxOpts
} from 'gridplus-sdk';

import { fMnemonicPhrase } from '../../../../.jest/__fixtures__';
import { getPathPrefix, stripHexPrefix, toChecksumAddress } from '../../../utils';

const hdNode = HDNode.fromMnemonic(fMnemonicPhrase);

const convertPathToString = (path: number[]): string =>
path
.map((p) => {
const withoutHardening = p - 0x80000000;
const index = withoutHardening >= 0 ? Math.min(p, withoutHardening) : p;
const isHardened = index === withoutHardening;
return `${index}${isHardened ? "'" : ''}`;
})
.join('/');

export class Client {
isPaired = false;
hasActiveWallet = jest.fn().mockReturnValue(true);
connect = jest
.fn()
.mockImplementation(
(_deviceID: string, callback: (err: Error | null, isPaired: boolean) => void) => {
this.isPaired = true;
callback(null, true);
}
);
sign = jest
.fn()
.mockImplementation(
async (opts: SignOpts, callback: (err: Error | null, data: SignResult) => void) => {
const path = convertPathToString(opts.data.signerPath);
const childNode = hdNode.derivePath(path);
const wallet = new Wallet(childNode.privateKey);
if (opts.currency === 'ETH') {
const { signerPath, chainId, ...transaction } = opts.data as SignTxOpts;

const isEIP1559 =
transaction.maxFeePerGas !== undefined &&
transaction.maxPriorityFeePerGas !== undefined;

const signedTransaction = await wallet.signTransaction({
...transaction,
chainId: parseInt((chainId as unknown) as string, 16),
type: isEIP1559 ? 2 : 0
});
const { v, r, s } = parse(signedTransaction) as Required<Transaction>;
callback(null, {
sig: {
// eslint-disable-next-line no-restricted-globals
v: v === 0 ? Buffer.from([]) : Buffer.from([v]),
r: stripHexPrefix(r),
s: stripHexPrefix(s)
}
});
} else if (opts.currency === 'ETH_MSG') {
const signMessageOpts = opts.data as SignMessageOpts;
const signature = await wallet.signMessage(arrayify(signMessageOpts.payload));
const { v, r, s } = splitSignature(signature);

callback(null, {
sig: {
// eslint-disable-next-line no-restricted-globals
v: v === 0 ? Buffer.from([]) : Buffer.from([v]),
r: stripHexPrefix(r),
s: stripHexPrefix(s)
}
});
}
}
);
getAddresses = jest
.fn()
.mockImplementation(
(opts: AddressesOpts, callback: (err: Error | null, data: string[]) => void) => {
const path = convertPathToString(opts.startPath);
const indices = path.split('/');
const offset = parseInt(indices[indices.length - 1]);

const masterNode = hdNode.derivePath(getPathPrefix(path));
const result = new Array(opts.n).fill(undefined).map((_, i) => {
const index = offset + i;
const node = masterNode.derivePath(index.toString(10));
return toChecksumAddress(node.address);
});
callback(null, result);
}
);
}
18 changes: 17 additions & 1 deletion src/implementations/deterministic/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TransportStatusError } from '@ledgerhq/errors';

import { WalletsErrorCode } from '../../types';
import { standardizeTrezorErr, wrapLedgerError } from './errors';
import { standardizeTrezorErr, wrapGridPlusError, wrapLedgerError } from './errors';

describe('wrapLedgerError', () => {
it('throws U2F timeout error', () => {
Expand Down Expand Up @@ -93,3 +93,19 @@ describe('standardizeTrezorErr', () => {
expect(err.message).toBe('Foo');
});
});

describe('wrapGridPlusError', () => {
it('throws timeout error', () => {
expect(() => wrapGridPlusError('Timeout waiting for device')).toThrow(
'Timeout waiting for device'
);
});

it('throws cancel error', () => {
expect(() => wrapGridPlusError('Request Declined by User')).toThrow('Request Declined by User');
});

it('throws unknown error', () => {
expect(() => wrapGridPlusError(new Error('Foo'))).toThrow('Foo');
});
});
16 changes: 16 additions & 0 deletions src/implementations/deterministic/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,19 @@ export const standardizeTrezorErr = (err: { error: string; code?: string }): Wal
// Other
return new WalletsError(err.error, WalletsErrorCode.UNKNOWN, new Error(err.error));
};

export const wrapGridPlusError = (err: Error | string) => {
throw standardizeGridPlusErr(err);
};

export const standardizeGridPlusErr = (err: Error | string): WalletsError => {
const message = typeof err === 'string' ? err : err.message;
if (message.includes('Timeout waiting for device')) {
return new WalletsError(message, WalletsErrorCode.TIMEOUT);
} else if (message.includes('Request Declined by User')) {
return new WalletsError(message, WalletsErrorCode.CANCELLED);
}

// Other
return new WalletsError(message, WalletsErrorCode.UNKNOWN, new Error(message));
};
Loading

0 comments on commit a6bff71

Please sign in to comment.