Merge pull request #18 from Instadapp/ledger

Add Ledger Connector
This commit is contained in:
Sowmay Jain 2021-09-08 01:40:48 +05:30 committed by GitHub
commit af78644f67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2040 additions and 115 deletions

12
assets/icons/ledger.svg Normal file
View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35 35">
<defs/>
<defs/>
<g id="prefix__Group_26536" data-name="Group 26536" transform="translate(-80 -205)">
<path id="prefix__Shape" d="M23.588 0h-16v21.583h21.6v-16A5.585 5.585 0 0023.588 0z" class="prefix__cls-1" transform="translate(85.739 205)"/>
<path id="prefix__Path_8749" d="M8.342 0H5.585A5.585 5.585 0 000 5.585v2.757h8.342z" class="prefix__cls-1" data-name="Path 8749" transform="translate(80 205)"/>
<path id="prefix__Rectangle-path" d="M0 7.59h8.342v8.342H0z" class="prefix__cls-1" transform="translate(80 210.739)"/>
<path id="prefix__Path_8750" d="M15.18 23.451h2.757a5.585 5.585 0 005.585-5.6V15.18H15.18z" class="prefix__cls-1" data-name="Path 8750" transform="translate(91.478 216.478)"/>
<path id="prefix__Path_8751" d="M7.59 15.18h8.342v8.342H7.59z" class="prefix__cls-1" data-name="Path 8751" transform="translate(85.739 216.478)"/>
<path id="prefix__Path_8752" d="M0 15.18v2.757a5.585 5.585 0 005.585 5.585h2.757V15.18z" class="prefix__cls-1" data-name="Path 8752" transform="translate(80 216.478)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -23,6 +23,7 @@
<button
class="w-full px-6 py-3 text-left flex items-center h-[80px] border border-[#DBE5F4] rounded-[4px] text-lg text-[#374253] font-semibold hover:bg-background-light"
v-for="(wallet, key) in wallets"
:disabled="connecting"
:key="key"
@click="connect(wallet.connector)"
>
@ -71,10 +72,12 @@ import { computed, defineComponent, ref } from '@nuxtjs/composition-api'
import Input from '~/components/common/input/Input.vue'
import { useModal } from '~/composables/useModal'
import { useWeb3 } from '@instadapp/vue-web3'
import { injected } from '~/connectors'
import { injected, ledger } from '~/connectors'
import { SUPPORTED_WALLETS } from '~/constant/wallet'
import ButtonCTA from '../../common/input/ButtonCTA.vue'
import ButtonCTAOutlined from '../../common/input/ButtonCTAOutlined.vue'
import { Network, useNetwork } from '~/composables/useNetwork'
import { useNotification } from '~/composables/useNotification'
export default defineComponent({
props: {
@ -87,11 +90,27 @@ export default defineComponent({
setup() {
const { close } = useModal()
const { activate } = useWeb3()
const { activeNetworkId } = useNetwork()
const { showError, showAwaiting, closeAll } = useNotification()
const connecting = ref(false)
const connect = async (connector) => {
await activate(connector, console.log)
connecting.value = true
showAwaiting("Connecting...")
try {
await activate(connector, undefined, true)
connecting.value = false
close()
closeAll()
} catch (error) {
closeAll()
showError("", error.message)
}
connecting.value = false
close()
}
const isMetamask = computed(() => process.server ? false : window.ethereum && window.ethereum.isMetaMask)
@ -102,6 +121,10 @@ export default defineComponent({
return null
}
if (wallet.connector === ledger && activeNetworkId.value !== Network.Mainnet) {
return null
}
return wallet
}).filter(Boolean))
@ -111,6 +134,7 @@ export default defineComponent({
wallets,
isMetamask,
injected,
connecting,
}
},
})

View File

@ -3,6 +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 "./ledger-connector";
import INSTADAPP_LOGO_URL from "~/assets/logo/instadapp-logo-icon.svg?inline";
@ -44,3 +46,20 @@ if (process.client) {
}
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}`,
requestTimeoutMs: 60000,
pollingInterval: POLLING_INTERVAL,
baseDerivationPath: "44'/60'/x'/0/0",
accountsOffset: 0,
accountsLength: 1
});

View 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();
}
}
}

View 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;
}

View File

@ -1,10 +1,11 @@
import { AbstractConnector } from '@web3-react/abstract-connector'
import { injected, walletconnect, portis, walletlink } from '~/connectors'
import { injected, walletconnect, portis, walletlink, ledger } from '~/connectors'
import METAMASK_ICON_URL from '~/assets/icons/metamask.svg?inline'
import WALLETCONNECT_ICON_URL from '~/assets/icons/wallet-connect-icon.svg?inline'
import PORTIS_ICON_URL from '~/assets/icons/portis.svg?inline'
import COINBASE_ICON_URL from '~/assets/icons/coinbase.svg?inline'
import LEDGER_ICON_URL from '~/assets/icons/ledger.svg?inline'
interface WalletInfo {
connector?: AbstractConnector;
@ -34,5 +35,10 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
name: 'Coinbase Wallet',
iconURL: COINBASE_ICON_URL,
},
LEDGER: {
connector: ledger,
name: 'Ledger',
iconURL: LEDGER_ICON_URL,
},
}

View File

@ -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",
@ -22,6 +23,7 @@
"@walletconnect/web3-provider": "^1.4.1",
"@web3-react/core": "^6.1.9",
"@web3-react/injected-connector": "^6.0.7",
"@web3-react/ledger-connector": "^6.1.9",
"@web3-react/portis-connector": "^6.1.9",
"@web3-react/walletconnect-connector": "^6.2.4",
"@web3-react/walletlink-connector": "^6.2.3",

1844
yarn.lock

File diff suppressed because it is too large Load Diff