mirror of
https://github.com/Instadapp/assembly.git
synced 2024-07-29 22:37:06 +00:00
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
|
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;
|
||
|
}
|