mirror of
https://github.com/Instadapp/assembly.git
synced 2024-07-29 22:37:06 +00:00
commit
af78644f67
12
assets/icons/ledger.svg
Normal file
12
assets/icons/ledger.svg
Normal 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 |
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user