UNPKG

saepenatus

Version:

Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardised spec compliant web3 providers for all supported wallets, framework agnostic modern javascript UI with code splitting, CSS customization, mul

305 lines (254 loc) 9.1 kB
import type { Chain, CustomNetwork, Platform, WalletInit } from '@web3-onboard/common' import type { Account, ScanAccountsOptions } from '@web3-onboard/hw-common' import type { StaticJsonRpcProvider } from '@ethersproject/providers' const DEFAULT_BASE_PATH = "m/44'/60'/0'/0" const basePaths = [ { label: 'Keystone', value: DEFAULT_BASE_PATH } ] const assets = [ { label: 'ETH' } ] const getAccount = async ( keyring: any, provider: StaticJsonRpcProvider, index: number ): Promise<Account> => { const address = (await keyring.addAccounts())[index] const derivationPath = await keyring._pathFromAddress(address) return { derivationPath, address, balance: { asset: '', value: await provider.getBalance(address) } } } const generateAccounts = async ( keyring: any, provider: StaticJsonRpcProvider ): Promise<Account[]> => { const accounts = [] let zeroBalanceAccounts = 0, index = 0 while (zeroBalanceAccounts < 5) { const account = await getAccount(keyring, provider, index) if (account.balance.value.isZero()) { zeroBalanceAccounts++ accounts.push(account) } else { accounts.push(account) // Reset the number of 0 balance accounts zeroBalanceAccounts = 0 } index++ } return accounts } function keystone({ customNetwork, filter, containerElement }: { customNetwork?: CustomNetwork filter?: Platform[] containerElement?: string } = {}): WalletInit { const getIcon = async () => (await import('./icon.js')).default return ({ device }) => { let accounts: Account[] | undefined const filtered = Array.isArray(filter) && (filter.includes(device.type) || filter.includes(device.os.name)) if (filtered) return null return { label: 'Keystone', getIcon, getInterface: async ({ EventEmitter, chains }) => { const { StaticJsonRpcProvider } = await import( '@ethersproject/providers' ) let { default: AirGappedKeyring } = await import( '@keystonehq/eth-keyring' ) // Super weird esm issue where the default export is an object with a property default on it // if that is the case then we just grab the default value // @ts-ignore AirGappedKeyring = 'default' in AirGappedKeyring ? // @ts-ignore AirGappedKeyring.default : AirGappedKeyring const { TransactionFactory: Transaction } = await import( '@ethereumjs/tx' ) const { createEIP1193Provider, ProviderRpcError, ProviderRpcErrorCode } = await import('@web3-onboard/common') const { accountSelect, getCommon, bigNumberFieldsToStrings, getHardwareWalletProvider } = await import('@web3-onboard/hw-common') const keyring = AirGappedKeyring.getEmptyKeyring() await keyring.readKeyring() const eventEmitter = new EventEmitter() let ethersProvider: StaticJsonRpcProvider let currentChain: Chain = chains[0] const scanAccounts = async ({ chainId }: ScanAccountsOptions): Promise<Account[]> => { currentChain = chains.find(({ id }: Chain) => id === chainId) || currentChain ethersProvider = new StaticJsonRpcProvider(currentChain.rpcUrl) return generateAccounts(keyring, ethersProvider) } const getAccounts = async () => { accounts = await accountSelect({ basePaths, assets, chains, scanAccounts, supportsCustomPath: false, containerElement }) if (accounts.length) { eventEmitter.emit('accountsChanged', [accounts[0].address]) } return accounts } const signMessage = (address: string, message: string) => { if (!(accounts && accounts.length && accounts.length > 0)) throw new Error( 'No account selected. Must call eth_requestAccounts first.' ) const account = accounts.find(account => account.address === address) || accounts[0] return keyring.signMessage(account.address, message) } const keystoneProvider = getHardwareWalletProvider( () => currentChain.rpcUrl ) const provider = createEIP1193Provider(keystoneProvider, { eth_requestAccounts: async () => { // Triggers the account select modal if no accounts have been selected const accounts = await getAccounts() if (accounts.length === 0) { throw new ProviderRpcError({ code: ProviderRpcErrorCode.ACCOUNT_ACCESS_REJECTED, message: 'User rejected the request.' }) } return accounts[0] ? [accounts[0].address] : [] }, eth_selectAccounts: async () => { const accounts = await getAccounts() return accounts.map(({ address }) => address) }, eth_accounts: async () => accounts && accounts[0].address ? [accounts[0].address] : [], eth_chainId: async () => currentChain.id, eth_signTransaction: async ({ params: [transactionObject] }) => { if (!accounts) throw new Error( 'No account selected. Must call eth_requestAccounts first.' ) if (!transactionObject) throw new ProviderRpcError({ message: 'Invalid method parameters', code: ProviderRpcErrorCode.INVALID_PARAMS, data: transactionObject }) const account = accounts.find( account => account.address === transactionObject.from ) || accounts[0] const { address: from } = account // Set the `from` field to the currently selected account transactionObject = { ...transactionObject, from } const chainId = currentChain.hasOwnProperty('id') ? Number.parseInt(currentChain.id) : 1 const common = await getCommon({ customNetwork, chainId }) transactionObject.gasLimit = transactionObject.gas || transactionObject.gasLimit // 'gas' is an invalid property for the TransactionRequest type delete transactionObject.gas const signer = ethersProvider.getSigner(from) let populatedTransaction = bigNumberFieldsToStrings( await signer.populateTransaction(transactionObject) ) const transaction = Transaction.fromTxData(populatedTransaction, { common, freeze: false }) let signedTx try { // @ts-ignore signedTx = await keyring.signTransaction(from, transaction) } catch (error: any) { if (error.message && error.message.message) { throw new Error(error.message.message) } else { throw new Error(error) } } return `0x${signedTx.serialize().toString('hex')}` }, eth_sendTransaction: async ({ baseRequest, params }) => { const signedTx = await provider.request({ method: 'eth_signTransaction', params }) const transactionHash = await baseRequest({ method: 'eth_sendRawTransaction', params: [signedTx] }) return transactionHash as string }, eth_sign: async ({ params: [address, message] }) => signMessage(address, message), personal_sign: async ({ params: [message, address] }) => signMessage(address, message), eth_signTypedData: async ({ params: [address, typedData] }) => { if (!(accounts && accounts.length && accounts.length > 0)) throw new Error( 'No account selected. Must call eth_requestAccounts first.' ) const account = accounts.find(account => account.address === address) || accounts[0] return keyring.signTypedData(account.address, typedData) }, wallet_switchEthereumChain: async ({ params: [{ chainId }] }) => { currentChain = chains.find(({ id }) => id === chainId) || currentChain if (!currentChain) throw new Error('chain must be set before switching') eventEmitter.emit('chainChanged', currentChain.id) return null }, wallet_addEthereumChain: null }) provider.on = eventEmitter.on.bind(eventEmitter) return { provider } } } } } export default keystone