mirror of
https://github.com/Instadapp/assembly.git
synced 2024-07-29 22:37:06 +00:00
Custom ledger connector
This commit is contained in:
parent
0661410adf
commit
e8122a6e0a
|
@ -3,7 +3,8 @@ import { InjectedConnector } from "@web3-react/injected-connector";
|
|||
import { WalletConnectConnector } from "@web3-react/walletconnect-connector";
|
||||
import { PortisConnector } from "@web3-react/portis-connector";
|
||||
import { WalletLinkConnector } from "@web3-react/walletlink-connector";
|
||||
import { LedgerConnector } from "@web3-react/ledger-connector";
|
||||
// import { LedgerConnector } from "@web3-react/ledger-connector";
|
||||
import { LedgerConnector } from "./ledger-connector";
|
||||
|
||||
import INSTADAPP_LOGO_URL from "~/assets/logo/instadapp-logo-icon.svg?inline";
|
||||
|
||||
|
@ -48,8 +49,17 @@ export { gnosisSafe };
|
|||
|
||||
const POLLING_INTERVAL = 12000;
|
||||
|
||||
export enum LedgerDerivationPath {
|
||||
"Legacy" = "44'/60'/0'/x",
|
||||
"LedgerLive" = "44'/60'/x'/0/0"
|
||||
}
|
||||
|
||||
export const ledger = new LedgerConnector({
|
||||
chainId: 1,
|
||||
url: `https://mainnet.infura.io/v3/${process.env.INFURA_ID}`,
|
||||
pollingInterval: POLLING_INTERVAL
|
||||
requestTimeoutMs: 60,
|
||||
pollingInterval: POLLING_INTERVAL,
|
||||
baseDerivationPath: "44'/60'/x'/0/0",
|
||||
accountsOffset: 0,
|
||||
accountsLength: 1
|
||||
});
|
||||
|
|
102
connectors/ledger-connector.ts
Normal file
102
connectors/ledger-connector.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
// https://github.com/aave/aave-ui/blob/e0602e3560b2aabf86e8afd29db20c0ee8c249fc/src/libs/web3-data-provider/web3-providers/connectors/ledger-connector.ts
|
||||
|
||||
import { ConnectorUpdate } from '@web3-react/types';
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector';
|
||||
import Web3ProviderEngine from 'web3-provider-engine';
|
||||
import { RPCSubprovider } from '@0x/subproviders/lib/src/subproviders/rpc_subprovider'; // https://github.com/0xProject/0x-monorepo/issues/1400
|
||||
import createLedgerSubprovider from './ledger-subprovider';
|
||||
import TransportU2F from '@ledgerhq/hw-transport-u2f';
|
||||
import webUsbTransport from '@ledgerhq/hw-transport-webusb';
|
||||
import type Transport from '@ledgerhq/hw-transport';
|
||||
|
||||
interface LedgerConnectorArguments {
|
||||
chainId: number;
|
||||
url: string;
|
||||
pollingInterval?: number;
|
||||
requestTimeoutMs?: number;
|
||||
baseDerivationPath?: string;
|
||||
accountsOffset?: number;
|
||||
accountsLength: number;
|
||||
}
|
||||
|
||||
const getTransport = async (): Promise<Transport> => {
|
||||
if (await webUsbTransport.isSupported()) {
|
||||
return await webUsbTransport.create();
|
||||
}
|
||||
return await TransportU2F.create();
|
||||
};
|
||||
|
||||
export class LedgerConnector extends AbstractConnector {
|
||||
private readonly chainId: number;
|
||||
private readonly url: string;
|
||||
private readonly pollingInterval?: number;
|
||||
private readonly requestTimeoutMs?: number;
|
||||
private readonly baseDerivationPath?: string;
|
||||
private readonly accountsOffset?: number;
|
||||
private readonly accountsLength: number;
|
||||
|
||||
private provider: any;
|
||||
|
||||
constructor({
|
||||
chainId,
|
||||
url,
|
||||
pollingInterval,
|
||||
requestTimeoutMs,
|
||||
baseDerivationPath,
|
||||
accountsOffset = 0,
|
||||
accountsLength = 1,
|
||||
}: LedgerConnectorArguments) {
|
||||
super({ supportedChainIds: [chainId] });
|
||||
|
||||
this.chainId = chainId;
|
||||
this.url = url;
|
||||
this.requestTimeoutMs = requestTimeoutMs;
|
||||
this.baseDerivationPath = baseDerivationPath;
|
||||
this.pollingInterval = pollingInterval;
|
||||
this.accountsOffset = accountsOffset;
|
||||
this.accountsLength = accountsLength;
|
||||
}
|
||||
|
||||
public async activate(): Promise<ConnectorUpdate> {
|
||||
if (!this.provider) {
|
||||
const engine = new Web3ProviderEngine({ pollingInterval: this.pollingInterval });
|
||||
const ledgerProvider = await createLedgerSubprovider(getTransport, {
|
||||
networkId: this.chainId,
|
||||
paths: this.baseDerivationPath ? [this.baseDerivationPath] : undefined,
|
||||
accountsLength: this.accountsLength,
|
||||
accountsOffset: this.accountsOffset,
|
||||
});
|
||||
engine.addProvider(ledgerProvider);
|
||||
engine.addProvider(new RPCSubprovider(this.url, this.requestTimeoutMs));
|
||||
this.provider = engine;
|
||||
this.provider.start();
|
||||
}
|
||||
const account = await this.getAccount();
|
||||
return { provider: this.provider, chainId: this.chainId, account };
|
||||
}
|
||||
|
||||
public async getProvider(): Promise<Web3ProviderEngine> {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
public async getChainId(): Promise<number> {
|
||||
return this.chainId;
|
||||
}
|
||||
|
||||
public async getAccount(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.provider._providers[0].getAccounts(function (error: any, result: string[]) {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public deactivate() {
|
||||
if (this.provider) {
|
||||
this.provider.stop();
|
||||
}
|
||||
}
|
||||
}
|
138
connectors/ledger-subprovider.ts
Normal file
138
connectors/ledger-subprovider.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
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;
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
"@gnosis.pm/safe-apps-web3-react": "^0.6.2",
|
||||
"@gnosis.pm/safe-apps-web3modal": "^2.0.0",
|
||||
"@instadapp/vue-web3": "^0.3.0",
|
||||
"@ledgerhq/hw-transport-webusb": "^6.6.0",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@nuxtjs/composition-api": "^0.27.0",
|
||||
"@portis/web3": "^4.0.5",
|
||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -1349,11 +1349,26 @@
|
|||
"@ledgerhq/logs" "^4.72.0"
|
||||
rxjs "^6.5.3"
|
||||
|
||||
"@ledgerhq/devices@^6.3.0":
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-6.3.0.tgz#7ee59614198882311d1805912e368451527d05b2"
|
||||
integrity sha512-DmVxqMAf3FhkpKjkbBCFVJ5DmesfplujeCLzFwO/zF5VGuwY7xxPqeSxlpusXJkqhEq+DbFzIDRWJYDf7rtXqg==
|
||||
dependencies:
|
||||
"@ledgerhq/errors" "^6.2.0"
|
||||
"@ledgerhq/logs" "^6.2.0"
|
||||
rxjs "6"
|
||||
semver "^7.3.5"
|
||||
|
||||
"@ledgerhq/errors@^4.78.0":
|
||||
version "4.78.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-4.78.0.tgz#23daf3af54d03b1bda3e616002b555da1bdb705a"
|
||||
integrity sha512-FX6zHZeiNtegBvXabK6M5dJ+8OV8kQGGaGtuXDeK/Ss5EmG4Ltxc6Lnhe8hiHpm9pCHtktOsnUVL7IFBdHhYUg==
|
||||
|
||||
"@ledgerhq/errors@^6.2.0":
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.2.0.tgz#7dc2b3bf6bdedccdaa1b97dccacfa912c4fc22f8"
|
||||
integrity sha512-eO03x8HJmG60WtlrMuahigW/rwywFdcGzCnihta/MjkM8BD9A660cKVkyIuheCcpaB7UV/r+QsRl9abHbjjaag==
|
||||
|
||||
"@ledgerhq/hw-app-eth@^4.3.0":
|
||||
version "4.78.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.78.0.tgz#fbd7ffe7f371d0c32a53f38c5149ab8d13514297"
|
||||
|
@ -1395,6 +1410,16 @@
|
|||
"@ledgerhq/hw-transport" "^4.24.0"
|
||||
u2f-api "0.2.7"
|
||||
|
||||
"@ledgerhq/hw-transport-webusb@^6.6.0":
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.6.0.tgz#9be0d9f32ccee27f754bc50b35d9a703ebcc4d36"
|
||||
integrity sha512-ovY2IzbmAFsVgym7ftLU4c36Me5DHuM41rdI7XipMRd1COvnYNiutqAMym2RLk5IS2LRPAqTWiWgt86Oy6DGvQ==
|
||||
dependencies:
|
||||
"@ledgerhq/devices" "^6.3.0"
|
||||
"@ledgerhq/errors" "^6.2.0"
|
||||
"@ledgerhq/hw-transport" "^6.3.0"
|
||||
"@ledgerhq/logs" "^6.2.0"
|
||||
|
||||
"@ledgerhq/hw-transport@^4.24.0", "@ledgerhq/hw-transport@^4.78.0":
|
||||
version "4.78.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.78.0.tgz#714786658e1f2fbc0569e06e2abf8d15d310d931"
|
||||
|
@ -1404,11 +1429,25 @@
|
|||
"@ledgerhq/errors" "^4.78.0"
|
||||
events "^3.0.0"
|
||||
|
||||
"@ledgerhq/hw-transport@^6.3.0":
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.3.0.tgz#4fc966b1a68c991c0a6b5384841f99c4f8304ce9"
|
||||
integrity sha512-kdnVrgmxrFtKaRdkoaQBEa02RXgLzEBiooYbxA65BGSJig3PGWDS9LrqNpzLTZM1RQlivd9NLBmfwU2ze4chWA==
|
||||
dependencies:
|
||||
"@ledgerhq/devices" "^6.3.0"
|
||||
"@ledgerhq/errors" "^6.2.0"
|
||||
events "^3.3.0"
|
||||
|
||||
"@ledgerhq/logs@^4.72.0":
|
||||
version "4.72.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-4.72.0.tgz#43df23af013ad1135407e5cf33ca6e4c4c7708d5"
|
||||
integrity sha512-o+TYF8vBcyySRsb2kqBDv/KMeme8a2nwWoG+lAWzbDmWfb2/MrVWYCVYDYvjXdSoI/Cujqy1i0gIDrkdxa9chA==
|
||||
|
||||
"@ledgerhq/logs@^6.2.0":
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.2.0.tgz#9fb2d6f1811316697f7b3cc14607f6c608912419"
|
||||
integrity sha512-SLyFyD7ElMhgKWPYedFGCT/ilcbGPgL5hXXYHxOM79Fs5fWi0zaUpt5oGqGMsOAAFaMa9/rbun0pokzPhEFz8A==
|
||||
|
||||
"@metamask/safe-event-emitter@2.0.0", "@metamask/safe-event-emitter@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c"
|
||||
|
@ -12328,7 +12367,7 @@ rustbn.js@~0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca"
|
||||
integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==
|
||||
|
||||
rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.3:
|
||||
rxjs@6, rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.3:
|
||||
version "6.6.7"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
|
||||
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
|
||||
|
|
Loading…
Reference in New Issue
Block a user