MyCrypto/common/libs/wallet/deterministic/ledger.ts

146 lines
3.8 KiB
TypeScript

import EthTx, { TxObj } from 'ethereumjs-tx';
import { addHexPrefix, toBuffer } from 'ethereumjs-util';
import TransportU2F from '@ledgerhq/hw-transport-u2f';
import LedgerEth from '@ledgerhq/hw-app-eth';
import { translateRaw } from 'translations';
import { getTransactionFields } from 'libs/transaction';
import { HardwareWallet, ChainCodeResponse } from './hardware';
// Ledger throws a few types of errors
interface U2FError {
metaData: {
type: string;
code: number;
};
}
interface ErrorWithId {
id: string;
message: string;
name: string;
stack: string;
}
type LedgerError = U2FError | ErrorWithId | Error | string;
export class LedgerWallet extends HardwareWallet {
public static async getChainCode(dpath: string): Promise<ChainCodeResponse> {
return makeApp()
.then(app => app.getAddress(dpath, false, true))
.then(res => {
return {
publicKey: res.publicKey,
chainCode: res.chainCode
};
})
.catch((err: LedgerError) => {
throw new Error(ledgerErrToMessage(err));
});
}
constructor(address: string, dPath: string, index: number) {
super(address, dPath, index);
}
public async signRawTransaction(t: EthTx): Promise<Buffer> {
const txFields = getTransactionFields(t);
t.v = Buffer.from([t._chainId]);
t.r = toBuffer(0);
t.s = toBuffer(0);
try {
const ethApp = await makeApp();
const result = await ethApp.signTransaction(this.getPath(), t.serialize().toString('hex'));
const txToSerialize: TxObj = {
...txFields,
v: addHexPrefix(result.v),
r: addHexPrefix(result.r),
s: addHexPrefix(result.s)
};
return new EthTx(txToSerialize).serialize();
} catch (err) {
throw Error(err + '. Check to make sure contract data is on');
}
}
public async signMessage(msg: string): Promise<string> {
if (!msg) {
throw Error('No message to sign');
}
try {
const msgHex = Buffer.from(msg).toString('hex');
const ethApp = await makeApp();
const signed = await ethApp.signPersonalMessage(this.getPath(), msgHex);
const combined = addHexPrefix(signed.r + signed.s + signed.v.toString(16));
return combined;
} catch (err) {
throw new Error(ledgerErrToMessage(err));
}
}
public async displayAddress() {
const path = `${this.dPath}/${this.index}`;
try {
const ethApp = await makeApp();
await ethApp.getAddress(path, true, false);
return true;
} catch (err) {
console.error('Failed to display Ledger address:', err);
return false;
}
}
public getWalletType(): string {
return translateRaw('X_LEDGER');
}
}
async function makeApp() {
const transport = await TransportU2F.create();
return new LedgerEth(transport);
}
const isU2FError = (err: LedgerError): err is U2FError => !!err && !!(err as U2FError).metaData;
const isStringError = (err: LedgerError): err is string => typeof err === 'string';
const isErrorWithId = (err: LedgerError): err is ErrorWithId =>
err.hasOwnProperty('id') && err.hasOwnProperty('message');
function ledgerErrToMessage(err: LedgerError) {
// https://developers.yubico.com/U2F/Libraries/Client_error_codes.html
if (isU2FError(err)) {
// Timeout
if (err.metaData.code === 5) {
return translateRaw('LEDGER_TIMEOUT');
}
return err.metaData.type;
}
if (isStringError(err)) {
// Wrong app logged into
if (err.includes('6804')) {
return translateRaw('LEDGER_WRONG_APP');
}
// Ledger locked
if (err.includes('6801')) {
return translateRaw('LEDGER_LOCKED');
}
return err;
}
if (isErrorWithId(err)) {
// Browser doesn't support U2F
if (err.message.includes('U2F not supported')) {
return translateRaw('U2F_NOT_SUPPORTED');
}
}
// Other
return err.toString();
}