import * as core from "@shapeshiftoss/hdwallet-core"; import { AddEthereumChainParameter } from "@shapeshiftoss/hdwallet-core"; import _ from "lodash"; import * as eth from "./ethereum"; export function isTallyHo(wallet: core.HDWallet): wallet is TallyHoHDWallet { return _.isObject(wallet) && (wallet as any)._isTallyHo; } export class TallyHoHDWalletInfo implements core.HDWalletInfo, core.ETHWalletInfo { readonly _supportsETHInfo = true; private _ethAddress: string | null = null; public getVendor(): string { return "Tally Ho"; } public hasOnDevicePinEntry(): boolean { return false; } public hasOnDevicePassphrase(): boolean { return true; } public hasOnDeviceDisplay(): boolean { return true; } public hasOnDeviceRecovery(): boolean { return true; } public hasNativeShapeShift(): boolean { // It doesn't... yet? - never ever never ever ever return false; } public supportsBip44Accounts(): boolean { return false; // unsure } 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 { return undefined; } public async ethSupportsNetwork(chainId = 1): 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 TallyHoHDWallet implements core.HDWallet, core.ETHWallet { readonly _supportsETH = true; readonly _supportsETHInfo = true; readonly _supportsEthSwitchChain = false; readonly _supportsAvalanche = false; readonly _supportsOptimism = false; readonly _supportsPolygon = true; readonly _supportsGnosis = false; readonly _supportsArbitrum = true; readonly _supportsArbitrumNova = false; readonly _supportsBase = false; readonly _supportsBSC = false; readonly _isTallyHo = true; info: TallyHoHDWalletInfo & core.HDWalletInfo; ethAddress?: string | null; provider: any; constructor(provider: unknown) { this.info = new TallyHoHDWalletInfo(); this.provider = provider; } async getFeatures(): Promise> { return {}; } public async isLocked(): Promise { return !this.provider.tallyHo.isUnlocked(); } public getVendor(): string { return "Tally Ho"; } public async getModel(): Promise { return "Tally Ho"; } public async getLabel(): Promise { return "Tally Ho"; } 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 { // Keep an eye on the status of the refactor PR here: https://github.com/tallycash/extension/pull/1165/files. This will add offline signing support to Tally Ho, at which point this should return true. return false; } public supportsBroadcast(): boolean { return true; } public async clearSession(): Promise { // TODO: Can we lock Tally Ho from here? } public async ping(msg: core.Ping): Promise { // no ping function for Tally Ho, 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 Tally Ho } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendPassphrase(passphrase: string): Promise { // cannot send passphrase to Tally Ho. Could show the widget? } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendCharacter(charater: string): Promise { // no concept of sendCharacter in Tally Ho } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendWord(word: string): Promise { // no concept of sendWord in Tally Ho } public async cancel(): Promise { // no concept of cancel in Tally Ho } // 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 Tally Ho } // eslint-disable-next-line @typescript-eslint/no-unused-vars public async loadDevice(msg: core.LoadDevice): Promise { // TODO: Does Tally Ho 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 ethSupportsSecureTransfer(): Promise { return false; } public ethSupportsNativeShapeShift(): boolean { return false; } public async ethSwitchChain(params: AddEthereumChainParameter): Promise { try { // at this point, we know that we're in the context of a valid TallyHo provider await this.provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: params.chainId }] }); } catch (e: any) { const error: core.SerializedEthereumRpcError = e; console.error(error); // 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) { // TODO: TallyHo currently supports a finite number of chains natively (Mainnet + Polygon/Arbitrum/Optimism under feature flag), with no capabilities to add new chains } throw new Error(e); } } 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 { this.ethAddress ??= await eth.ethGetAddress(this.provider); return this.ethAddress; } 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 "tallyho:" + (await this.ethGetAddress(this.provider)); } public async getFirmwareVersion(): Promise { return "tallyho"; } }