import * as core from "@shapeshiftoss/hdwallet-core"; import * as eth from "./ethereum"; import _ from "lodash"; import detectEthereumProvider from "@metamask/detect-provider"; class TallyTransport extends core.Transport { public async getDeviceID() { return "tally:0"; } public call(...args: any[]): Promise { return Promise.resolve(); } } export function isTally(wallet: core.HDWallet): wallet is TallyHDWallet { return _.isObject(wallet) && (wallet as any)._isTally; } type HasNonTrivialConstructor = T extends { new (): any } ? never : T; export class TallyHDWallet implements core.HDWallet, core.ETHWallet { readonly _supportsETH = true; readonly _supportsETHInfo = true; readonly _supportsBTCInfo = false; readonly _supportsBTC = false; readonly _supportsCosmosInfo = false; readonly _supportsCosmos = false; readonly _supportsOsmosisInfo = false; readonly _supportsOsmosis = false; readonly _supportsBinanceInfo = false; readonly _supportsBinance = false; readonly _supportsDebugLink = false; readonly _isPortis = false; readonly _isTally = 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; transport: core.Transport = new TallyTransport(new core.Keyring()); info: TallyHDWalletInfo & core.HDWalletInfo; ethAddress?: string | null; provider: any; constructor() { this.info = new TallyHDWalletInfo(); } async getFeatures(): Promise> { return {}; } public async isLocked(): Promise { return !this.provider.tally.isUnlocked(); } public getVendor(): string { return "Tally"; } public getModel(): Promise { return Promise.resolve("Tally"); } public getLabel(): Promise { return Promise.resolve("Tally"); } public async initialize(): Promise { try { this.provider = await detectEthereumProvider({ mustBeMetaMask: false, silent: false, timeout: 3000 }); } catch (e) { console.error(e); } return Promise.resolve(); } 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 supportsOfflineSigning(): boolean { return false; } public supportsBroadcast(): boolean { return true; } public async clearSession(): Promise { // TODO: Can we lock Tally from here? } public ping(msg: core.Ping): Promise { // no ping function for Tally, so just returning Core.Pong return Promise.resolve({ msg: msg.msg }); } public sendPin(pin: string): Promise { // no concept of pin in Tally return Promise.resolve(); } public sendPassphrase(passphrase: string): Promise { // cannot send passphrase to Tally. Could show the widget? return Promise.resolve(); } public sendCharacter(charater: string): Promise { // no concept of sendCharacter in Tally return Promise.resolve(); } public sendWord(word: string): Promise { // no concept of sendWord in Tally return Promise.resolve(); } public cancel(): Promise { // no concept of cancel in Tally return Promise.resolve(); } public wipe(): Promise { return Promise.resolve(); } public reset(msg: core.ResetDevice): Promise { return Promise.resolve(); } public recover(msg: core.RecoverDevice): Promise { // no concept of recover in Tally return Promise.resolve(); } public loadDevice(msg: core.LoadDevice): Promise { // TODO: Does Tally allow this to be done programatically? return Promise.resolve(); } public describePath(msg: core.DescribePath): core.PathDescription { return this.info.describePath(msg); } public async getPublicKeys(msg: Array): Promise> { // Ethereum public keys are not exposed by the RPC API return []; } public async isInitialized(): Promise { return true; } public disconnect(): Promise { return Promise.resolve(); } public async ethSupportsNetwork(chainId: number = 1): Promise { return chainId === 1; } public async ethSupportsSecureTransfer(): Promise { return false; } public ethSupportsNativeShapeShift(): boolean { return false; } public async ethSupportsEIP1559(): Promise { return false; } public ethGetAccountPaths(msg: core.ETHGetAccountPath): Array { return eth.ethGetAccountPaths(msg); } public ethNextAccountPath(msg: core.ETHAccountPath): core.ETHAccountPath | undefined { return this.info.ethNextAccountPath(msg); } 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 "tally:" + (await this.ethGetAddress(this.provider)); } public async getFirmwareVersion(): Promise { return "tally"; } } export class TallyHDWalletInfo 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 "Tally"; } public hasOnDevicePinEntry(): boolean { return false; } public hasOnDevicePassphrase(): boolean { return true; } public hasOnDeviceDisplay(): boolean { return true; } public hasOnDeviceRecovery(): boolean { return true; } public hasNativeShapeShift(srcCoin: core.Coin, dstCoin: core.Coin): boolean { // It doesn't... yet? 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"); } } public ethNextAccountPath(msg: core.ETHAccountPath): core.ETHAccountPath | undefined { // TODO: What do we do here? return undefined; } public async ethSupportsNetwork(chainId: number = 1): Promise { return chainId === 1; } public async ethSupportsSecureTransfer(): Promise { return false; } public ethSupportsNativeShapeShift(): boolean { return false; } public async ethSupportsEIP1559(): Promise { return false; } public ethGetAccountPaths(msg: core.ETHGetAccountPath): Array { return eth.ethGetAccountPaths(msg); } } export function info() { return new TallyHDWalletInfo(); } export function create(): TallyHDWallet { return new TallyHDWallet(); }