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

559 lines (481 loc) 16.8 kB
import { Account, Asset, ScanAccountsOptions } from '@web3-onboard/hw-common' import type { StaticJsonRpcProvider } from '@ethersproject/providers' import type { TransactionRequest } from '@ethersproject/providers' import type { FeeMarketEIP1559TxData, TxData, FeeMarketEIP1559Transaction, Transaction } from '@ethereumjs/tx' // cannot be dynamically imported import { Buffer } from 'buffer' import type { Chain, CustomNetwork, Platform, TransactionObject, WalletInit } from '@web3-onboard/common' interface TrezorOptions { email: string appUrl: string customNetwork?: CustomNetwork filter?: Platform[] containerElement?: string } const TREZOR_DEFAULT_PATH = "m/44'/60'/0'/0" const assets = [ { label: 'ETH' } ] const DEFAULT_BASE_PATHS = [ { label: 'Ethereum Mainnet', value: TREZOR_DEFAULT_PATH } ] interface AccountData { publicKey: string chainCode: string path: string } const getAccount = async ( { publicKey, chainCode, path }: AccountData, asset: Asset, index: number, provider: StaticJsonRpcProvider ): Promise<Account> => { //@ts-ignore const { default: HDKey } = await import('hdkey') const ethUtil = await import('ethereumjs-util') // @ts-ignore - Commonjs importing weirdness const { publicToAddress, toChecksumAddress } = ethUtil.default || ethUtil const hdk = new HDKey() hdk.publicKey = Buffer.from(publicKey, 'hex') hdk.chainCode = Buffer.from(chainCode, 'hex') const dkey = hdk.deriveChild(index) const address = toChecksumAddress( `0x${publicToAddress(dkey.publicKey, true).toString('hex')}` ) return { derivationPath: `${path}/${index}`, address, balance: { asset: asset.label, value: await provider.getBalance(address) } } } const getAddresses = async ( account: AccountData, asset: Asset, provider: StaticJsonRpcProvider ): Promise<Account[]> => { const accounts = [] let index = 0 let zeroBalanceAccounts = 0 // Iterates until a 0 balance account is found // Then adds 4 more 0 balance accounts to the array while (zeroBalanceAccounts < 5) { const acc = await getAccount(account, asset, index, provider) if ( acc && acc.hasOwnProperty('balance') && acc.balance.hasOwnProperty('value') && acc.balance.value.isZero() ) { zeroBalanceAccounts++ accounts.push(acc) } else { accounts.push(acc) // Reset the number of 0 balance accounts zeroBalanceAccounts = 0 } index++ } return accounts } function trezor(options: TrezorOptions): WalletInit { const getIcon = async () => (await import('./icon.js')).default return ({ device }) => { const { email, appUrl, customNetwork, filter, containerElement } = options || {} if (!email || !appUrl) { throw new Error( 'Email and AppUrl required in Trezor options for Trezor Wallet Connection' ) } const filtered = Array.isArray(filter) && (filter.includes(device.type) || filter.includes(device.os.name)) if (filtered) return null let accounts: Account[] | undefined return { label: 'Trezor', getIcon, getInterface: async ({ EventEmitter, chains }) => { const { default: Trezor } = await import('trezor-connect') const { Transaction, FeeMarketEIP1559Transaction } = await import( '@ethereumjs/tx' ) const { createEIP1193Provider, ProviderRpcError } = await import( '@web3-onboard/common' ) const { accountSelect } = await import('@web3-onboard/hw-common') const { getCommon, bigNumberFieldsToStrings, getHardwareWalletProvider } = await import('@web3-onboard/hw-common') const ethUtil = await import('ethereumjs-util') const { compress } = (await import('eth-crypto')).publicKey const { StaticJsonRpcProvider } = await import( '@ethersproject/providers' ) // @ts-ignore const TrezorConnect = Trezor.default || Trezor TrezorConnect.manifest({ email: email, appUrl: appUrl }) const eventEmitter = new EventEmitter() let currentChain: Chain = chains[0] let account: | { publicKey: string; chainCode: string; path: string } | undefined let ethersProvider: StaticJsonRpcProvider const scanAccounts = async ({ derivationPath, chainId, asset }: ScanAccountsOptions): Promise<Account[]> => { currentChain = chains.find(({ id }) => id === chainId) || currentChain ethersProvider = new StaticJsonRpcProvider(currentChain.rpcUrl) const { publicKey, chainCode, path } = await getPublicKey( derivationPath ) if (derivationPath !== TREZOR_DEFAULT_PATH) { const address = await getAddress(path) return [ { derivationPath, address, balance: { asset: asset.label, value: await ethersProvider.getBalance(address.toLowerCase()) } } ] } return getAddresses( { publicKey: compress(publicKey), chainCode: chainCode || '', path: derivationPath }, asset, ethersProvider ) } const getAccountFromAccountSelect = async () => { accounts = await accountSelect({ basePaths: DEFAULT_BASE_PATHS, assets, chains, scanAccounts, containerElement }) if ( Array.isArray(accounts) && accounts.length && accounts[0].hasOwnProperty('address') ) { eventEmitter.emit('accountsChanged', [accounts[0].address]) } return accounts } async function getAddress(path: string) { const errorMsg = `Unable to derive address from path ${path}` try { const result = await TrezorConnect.ethereumGetAddress({ path, showOnTrezor: true }) if (!result.success) { throw new Error(errorMsg) } return result.payload.address } catch (error) { console.error(error) throw new Error(errorMsg) } } async function getPublicKey(dPath: string) { if (!dPath) { throw new Error('a derivation path is needed to get the public key') } try { const result = await TrezorConnect.getPublicKey({ path: dPath, coin: 'ETH' }) if (!result.success) { throw new Error(result.payload.error) } account = { publicKey: result.payload.publicKey, chainCode: result.payload.chainCode, path: result.payload.serializedPath } return account } catch (error) { throw new Error( `There was an error accessing your Trezor accounts - Error: ${error}` ) } } function createTrezorTransactionObject( transactionData: TransactionObject ): Partial<TransactionObject> { if ( !transactionData || (!transactionData.hasOwnProperty('gasLimit') && !transactionData.hasOwnProperty('gas')) ) { throw new Error( 'There was no Transaction Object or both the gasLimit and gas property are missing' ) } const gasLimit = transactionData.gasLimit || transactionData.gas if ( transactionData!.maxFeePerGas || transactionData!.maxPriorityFeePerGas ) { return { to: transactionData.to!, value: transactionData.value || '', gasLimit: gasLimit!, maxFeePerGas: transactionData.maxFeePerGas!, maxPriorityFeePerGas: transactionData.maxPriorityFeePerGas!, nonce: transactionData.nonce!, chainId: Number(currentChain.id), data: transactionData.hasOwnProperty('data') ? transactionData.data : '' } } return { to: transactionData.to!, value: transactionData.value || '', gasPrice: transactionData.gasPrice!, gasLimit: gasLimit!, nonce: transactionData.nonce!, chainId: Number(currentChain.id), data: transactionData.hasOwnProperty('data') ? transactionData.data : '' } } function trezorSignTransaction( path: string, transactionData: TransactionRequest ) { try { return TrezorConnect.ethereumSignTransaction({ path: path, transaction: transactionData }) } catch (error) { throw new Error( `There was an error signing transaction - Error: ${error}` ) } } async function signTransaction(transactionObject: TransactionObject) { if (!Array.isArray(accounts) || !accounts.length) throw new Error( 'No account selected. Must call eth_requestAccounts first.' ) let signingAccount if (transactionObject.hasOwnProperty('from')) { signingAccount = accounts.find( account => account.address === transactionObject.from ) } signingAccount = signingAccount ? signingAccount : accounts[0] const { derivationPath, address } = signingAccount transactionObject.gasLimit = transactionObject.gas || transactionObject.gasLimit // 'gas' is an invalid property for the TransactionRequest type delete transactionObject.gas const signer = ethersProvider.getSigner(address) const populatedTransaction = await signer.populateTransaction( transactionObject ) if ( populatedTransaction.hasOwnProperty('nonce') && typeof populatedTransaction.nonce === 'number' ) { populatedTransaction.nonce = populatedTransaction.nonce.toString(16) } if ( populatedTransaction.hasOwnProperty('nonce') && typeof populatedTransaction.nonce === 'string' ) { // Adds "0x" to a given `String` if it does not already start with "0x". populatedTransaction.nonce = ethUtil.addHexPrefix( populatedTransaction.nonce ) } const updateBigNumberFields = bigNumberFieldsToStrings(populatedTransaction) const transactionData = createTrezorTransactionObject( updateBigNumberFields as TransactionObject ) transactionData.from = address const chainId = currentChain.hasOwnProperty('id') ? Number(currentChain.id) : 1 const common = await getCommon({ customNetwork, chainId }) const trezorResult = await trezorSignTransaction( derivationPath, transactionData ) if (!trezorResult.success) { const message = trezorResult.payload.error === 'Unknown message' ? 'This type of transactions is not supported on this device' : trezorResult.payload.error throw new Error(message) } let signedTx: FeeMarketEIP1559Transaction | Transaction if ( transactionData!.maxFeePerGas || transactionData!.maxPriorityFeePerGas ) { signedTx = FeeMarketEIP1559Transaction.fromTxData( { ...(transactionData as FeeMarketEIP1559TxData), ...trezorResult.payload }, { common } ) } else { signedTx = Transaction.fromTxData( { ...(transactionData as TxData), ...trezorResult.payload }, { common } ) } return signedTx ? `0x${signedTx.serialize().toString('hex')}` : '' } async function signMessage( address: string, message: { data: string } ): Promise<string> { if (!Array.isArray(accounts) || !accounts.length) throw new Error( 'No account selected. Must call eth_requestAccounts first.' ) const accountToSign = accounts.find(account => account.address === address) || accounts[0] return new Promise((resolve, reject) => { TrezorConnect.ethereumSignMessage({ path: accountToSign.derivationPath, message: ethUtil.stripHexPrefix(message.data), hex: true }).then((response: any) => { if (response.success) { if ( response.payload.address !== ethUtil.toChecksumAddress(address) ) { reject(new Error('signature doesnt match the right address')) } const signature = `0x${response.payload.signature}` resolve(signature) } else { reject( new Error( (response.payload && response.payload.error) || 'There was an error signing a message' ) ) } }) }) } const trezorProvider = getHardwareWalletProvider( () => currentChain?.rpcUrl ) const provider = createEIP1193Provider(trezorProvider, { eth_requestAccounts: async () => { const accounts = await getAccountFromAccountSelect() if (!Array.isArray(accounts)) throw new Error( 'No account selected. Must call eth_requestAccounts first.' ) if (accounts.length === 0) { throw new ProviderRpcError({ code: 4001, message: 'User rejected the request.' }) } if (!accounts[0].hasOwnProperty('address')) throw new Error( 'No address property associated with the selected account' ) return [accounts[0].address] }, eth_selectAccounts: async () => { const accounts = await getAccountFromAccountSelect() return accounts.map(({ address }) => address) }, eth_accounts: async () => Array.isArray(accounts) && accounts.length && accounts[0].hasOwnProperty('address') ? [accounts[0].address] : [], eth_chainId: async () => currentChain.hasOwnProperty('id') ? currentChain.id : '', eth_signTransaction: async ({ params: [transactionObject] }) => signTransaction(transactionObject), 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, { data: message }), personal_sign: async ({ params: [message, address] }) => signMessage(address, { data: message }), 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 }, eth_signTypedData: null, wallet_addEthereumChain: null }) provider.on = eventEmitter.on.bind(eventEmitter) return { provider } } } } } export default trezor