assembly/connectors/ledger-subprovider.ts

138 lines
4.5 KiB
TypeScript
Raw Permalink Normal View History

2021-09-07 17:00:30 +00:00
import AppEth from '@ledgerhq/hw-app-eth';
import type Transport from '@ledgerhq/hw-transport';
import HookedWalletSubprovider from 'web3-provider-engine/subproviders/hooked-wallet';
import stripHexPrefix from 'strip-hex-prefix';
import { Transaction as EthereumTx } from 'ethereumjs-tx';
function makeError(msg: string, id: string) {
const err: any = new Error(msg);
err.id = id;
return err;
}
/**
*/
type SubproviderOptions = {
// refer to https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
networkId: number;
// derivation path schemes (with a x in the path)
paths?: string[];
// should use actively validate on the device
askConfirm?: boolean;
// number of accounts to derivate
accountsLength?: number;
// offset index to use to start derivating the accounts
accountsOffset?: number;
};
const defaultOptions = {
networkId: 1, // mainnet
paths: ["44'/60'/x'/0/0", "44'/60'/0'/x"], // ledger live derivation path
askConfirm: false,
accountsLength: 1,
accountsOffset: 0,
};
/**
* Create a HookedWalletSubprovider for Ledger devices.
*/
export default async function createLedgerSubprovider(
getTransport: () => Promise<Transport>,
options?: SubproviderOptions
) {
if (options && 'path' in options) {
throw new Error(
"@ledgerhq/web3-subprovider: path options was replaced by paths. example: paths: [\"44'/60'/x'/0/0\"]"
);
}
const { networkId, paths, askConfirm, accountsLength, accountsOffset } = {
...defaultOptions,
...options,
};
if (!paths.length) {
throw new Error('paths must not be empty');
}
const addressToPathMap: any = {};
const transport = await getTransport();
async function getAccounts() {
const eth = new AppEth(transport);
const addresses: any = {};
for (let i = accountsOffset; i < accountsOffset + accountsLength; i++) {
const x = Math.floor(i / paths.length);
const pathIndex = i - paths.length * x;
const path = paths[pathIndex].replace('x', String(x));
const address = await eth.getAddress(path, askConfirm, false);
addresses[path] = address.address;
addressToPathMap[address.address.toLowerCase()] = path;
}
return addresses;
}
async function signPersonalMessage(msgData: any) {
const path = addressToPathMap[msgData.from.toLowerCase()];
if (!path) throw new Error("address unknown '" + msgData.from + "'");
const eth = new AppEth(transport);
const result = await eth.signPersonalMessage(path, stripHexPrefix(msgData.data));
const v = parseInt(result.v, 10) - 27;
let vHex = v.toString(16);
if (vHex.length < 2) {
vHex = `0${v}`;
}
return `0x${result.r}${result.s}${vHex}`;
}
async function signTransaction(txData: any) {
const path = addressToPathMap[txData.from.toLowerCase()];
if (!path) throw new Error("address unknown '" + txData.from + "'");
const eth = new AppEth(transport);
const tx = new EthereumTx(txData, { chain: networkId });
// Set the EIP155 bits
tx.raw[6] = Buffer.from([networkId]); // v
tx.raw[7] = Buffer.from([]); // r
tx.raw[8] = Buffer.from([]); // s
// Pass hex-rlp to ledger for signing
const result = await eth.signTransaction(path, tx.serialize().toString('hex'));
// Store signature in transaction
tx.v = Buffer.from(result.v, 'hex');
tx.r = Buffer.from(result.r, 'hex');
tx.s = Buffer.from(result.s, 'hex');
// EIP155: v should be chain_id * 2 + {35, 36}
const signedChainId = Math.floor((tx.v[0] - 35) / 2);
const validChainId = networkId & 0xff; // FIXME this is to fixed a current workaround that app don't support > 0xff
if (signedChainId !== validChainId) {
throw makeError(
'Invalid networkId signature returned. Expected: ' + networkId + ', Got: ' + signedChainId,
'InvalidNetworkId'
);
}
return `0x${tx.serialize().toString('hex')}`;
}
const subprovider = new HookedWalletSubprovider({
getAccounts: (callback: (err: any, res: any) => void) => {
getAccounts()
.then((res) => callback(null, Object.values(res)))
.catch((err) => callback(err, null));
},
signPersonalMessage: (txData: any, callback: (err: any, res: any) => void) => {
signPersonalMessage(txData)
.then((res) => callback(null, res))
.catch((err) => callback(err, null));
},
signTransaction: (txData: any, callback: (err: any, res: any) => void) => {
signTransaction(txData)
.then((res) => callback(null, res))
.catch((err) => callback(err, null));
},
});
return subprovider;
}