import * as core from "@shapeshiftoss/hdwallet-core"; import { AddEthereumChainParameter } from "@shapeshiftoss/hdwallet-core"; import { ethErrors, serializeError } from "eth-rpc-errors"; import _ from "lodash"; import * as eth from "./ethereum"; export function isMetaMask(wallet: core.HDWallet): wallet is MetaMaskHDWallet { return _.isObject(wallet) && (wallet as any)._isMetaMask; } export class MetaMaskHDWalletInfo implements core.HDWalletInfo, core.ETHWalletInfo { readonly _supportsBTCInfo = false; readonly _supportsETHInfo = true; readonly _supportsCosmosInfo = false; readonly _supportsBinanceInfo = false; readonly _supportsRippleInfo = false; readonly _supportsEosInfo = false; readonly _supportsFioInfo = false; readonly _supportsThorchainInfo = false; readonly _supportsSecretInfo = false; readonly _supportsKavaInfo = false; readonly _supportsTerraInfo = false; public getVendor(): string { return "MetaMask"; } public hasOnDevicePinEntry(): boolean { return false; } public hasOnDevicePassphrase(): boolean { return true; } public hasOnDeviceDisplay(): boolean { return true; } public hasOnDeviceRecovery(): boolean { return true; } // eslint-disable-next-line @typescript-eslint/no-unused-vars public hasNativeShapeShift(srcCoin: core.Coin, dstCoin: core.Coin): boolean { return false; } public supportsBip44Accounts(): boolean { return false; } public supportsOfflineSigning(): boolean { return false; } public supportsBroadcast(): boolean { return true; } public describePath(msg: core.DescribePath): core.PathDescription { switch (msg.coin) { case "Ethereum": return eth.describeETHPath(msg.path); default: throw new Error("Unsupported path"); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars public ethNextAccountPath(msg: core.ETHAccountPath): core.ETHAccountPath | undefined { // TODO: What do we do here? return undefined; } public async ethSupportsNetwork(chainId: number): Promise { return chainId === 1; } public async ethSupportsSecureTransfer(): Promise { return false; } public ethSupportsNativeShapeShift(): boolean { return false; } public async ethSupportsEIP1559(): Promise { return true; } public ethGetAccountPaths(msg: core.ETHGetAccountPath): Array { return eth.ethGetAccountPaths(msg); } } export class MetaMaskHDWallet implements core.HDWallet, core.ETHWallet { readonly _supportsETH = true; readonly _supportsETHInfo = true; readonly _supportsBTCInfo = false; readonly _supportsBTC = false; readonly _supportsCosmosInfo = false; readonly _supportsCosmos = false; readonly _supportsEthSwitchChain = true; readonly _supportsAvalanche = true; readonly _supportsOptimism = true; readonly _supportsBSC = true; readonly _supportsPolygon = true; readonly _supportsGnosis = true; readonly _supportsArbitrum = true; readonly _supportsArbitrumNova = true; readonly _supportsBase = true; readonly _supportsOsmosisInfo = false; readonly _supportsOsmosis = false; readonly _supportsBinanceInfo = false; readonly _supportsBinance = false; readonly _supportsDebugLink = false; readonly _isPortis = false; readonly _isMetaMask = true; readonly _supportsRippleInfo = false; readonly _supportsRipple = false; readonly _supportsEosInfo = false; readonly _supportsEos = false; readonly _supportsFioInfo = false; readonly _supportsFio = false; readonly _supportsThorchainInfo = false; readonly _supportsThorchain = false; readonly _supportsSecretInfo = false; readonly _supportsSecret = false; readonly _supportsKava = false; readonly _supportsKavaInfo = false; readonly _supportsTerra = false; readonly _supportsTerraInfo = false; info: MetaMaskHDWalletInfo & core.HDWalletInfo; ethAddress?: string | null; provider: any; constructor(provider: unknown) { this.info = new MetaMaskHDWalletInfo(); this.provider = provider; } async getFeatures(): Promise> { return {}; } public async isLocked(): Promise { return !this.provider._metamask.isUnlocked(); } public getVendor(): string { return "MetaMask"; } public async getModel(): Promise { return "MetaMask"; } public async getLabel(): Promise { return "MetaMask"; } public async initialize(): Promise { // nothing to initialize } public hasOnDevicePinEntry(): boolean { return this.info.hasOnDevicePinEntry(); } public hasOnDevicePassphrase(): boolean { return this.info.hasOnDevicePassphrase(); } public hasOnDeviceDisplay(): boolean { return this.info.hasOnDeviceDisplay(); } public hasOnDeviceRecovery(): boolean { return this.info.hasOnDeviceRecovery(); } public hasNativeShapeShift(srcCoin: core.Coin, dstCoin: core.Coin): boolean { return this.info.hasNativeShapeShift(srcCoin, dstCoin); } public supportsBip44Accounts(): boolean { return this.info.supportsBip44Accounts(); } public supportsOfflineSigning(): boolean { return false; } public supportsBroadcast(): boolean { return true; } public async clearSession(): Promise { // TODO: Can we lock MetaMask from here? } public async ping(msg: core.Ping): Promise { // no ping function for MetaMask, so just returning Core.Pong return { msg: msg.msg }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendPin(pin: string): Promise { // no concept of pin in MetaMask } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendPassphrase(passphrase: string): Promise { // cannot send passphrase to MetaMask. Could show the widget? } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendCharacter(charater: string): Promise { // no concept of sendCharacter in MetaMask } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendWord(word: string): Promise { // no concept of sendWord in MetaMask } public async cancel(): Promise { // no concept of cancel in MetaMask } // eslint-disable-next-line @typescript-eslint/no-empty-function public async wipe(): Promise {} // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars public async reset(msg: core.ResetDevice): Promise {} // eslint-disable-next-line @typescript-eslint/no-unused-vars public async recover(msg: core.RecoverDevice): Promise { // no concept of recover in MetaMask } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async loadDevice(msg: core.LoadDevice): Promise { // TODO: Does MetaMask allow this to be done programatically? } public describePath(msg: core.DescribePath): core.PathDescription { return this.info.describePath(msg); } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async getPublicKeys(msg: Array): Promise> { // Ethereum public keys are not exposed by the RPC API return []; } public async isInitialized(): Promise { return true; } // eslint-disable-next-line @typescript-eslint/no-empty-function public async disconnect(): Promise {} public async ethSupportsNetwork(chainId = 1): Promise { return chainId === 1; } public async ethGetChainId(): Promise { try { // chainId as hex string const chainId: string = await this.provider.request({ method: "eth_chainId" }); return parseInt(chainId, 16); } catch (e) { console.error(e); return null; } } public async ethAddChain(params: AddEthereumChainParameter): Promise { // at this point, we know that we're in the context of a valid MetaMask provider await this.provider.request({ method: "wallet_addEthereumChain", params: [params] }); } public async ethSwitchChain(params: AddEthereumChainParameter): Promise { try { // at this point, we know that we're in the context of a valid MetaMask provider await this.provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: params.chainId }] }); } catch (e: any) { const error = serializeError(e); // https://docs.metamask.io/guide/ethereum-provider.html#errors // Internal error, which in the case of wallet_switchEthereumChain call means the chain isn't currently added to the wallet if (error.code === -32603) { // We only support Avalanche C-Chain currently. It is supported natively in XDEFI, and unsupported in Tally, both with no capabilities to add a new chain // TODO(gomes): Find a better home for these. When that's done, we'll want to call ethSwitchChain with (params: AddEthereumChainParameter) instead try { await this.ethAddChain(params); return; } catch (addChainE: any) { const addChainError = serializeError(addChainE); if (addChainError.code === 4001) { throw ethErrors.provider.userRejectedRequest(); } throw (addChainError.data as any).originalError as { code: number; message: string; stack: string; }; } } if (error.code === 4001) { throw ethErrors.provider.userRejectedRequest(); } throw (error.data as any).originalError as { code: number; message: string; stack: string; }; } } public async ethSupportsSecureTransfer(): Promise { return false; } public ethSupportsNativeShapeShift(): boolean { return false; } public async ethSupportsEIP1559(): Promise { return true; } public ethGetAccountPaths(msg: core.ETHGetAccountPath): Array { return eth.ethGetAccountPaths(msg); } public ethNextAccountPath(msg: core.ETHAccountPath): core.ETHAccountPath | undefined { return this.info.ethNextAccountPath(msg); } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async ethGetAddress(_msg: core.ETHGetAddress): Promise { if (this.ethAddress) { return this.ethAddress; } const address = await eth.ethGetAddress(this.provider); if (address) { this.ethAddress = address; return address; } else { this.ethAddress = null; return null; } } public async ethSignTx(msg: core.ETHSignTx): Promise { const address = await this.ethGetAddress(this.provider); return address ? eth.ethSignTx(msg, this.provider, address) : null; } public async ethSendTx(msg: core.ETHSignTx): Promise { const address = await this.ethGetAddress(this.provider); return address ? eth.ethSendTx(msg, this.provider, address) : null; } public async ethSignMessage(msg: core.ETHSignMessage): Promise { const address = await this.ethGetAddress(this.provider); return address ? eth.ethSignMessage(msg, this.provider, address) : null; } public async ethVerifyMessage(msg: core.ETHVerifyMessage): Promise { return eth.ethVerifyMessage(msg, this.provider); } public async getDeviceID(): Promise { return "metaMask:" + (await this.ethGetAddress(this.provider)); } public async getFirmwareVersion(): Promise { return "metaMask"; } }