diff --git a/examples/sandbox/index.html b/examples/sandbox/index.html
index ef998ec9f..8f0c07917 100644
--- a/examples/sandbox/index.html
+++ b/examples/sandbox/index.html
@@ -116,7 +116,8 @@
Select
-
+
+
diff --git a/examples/sandbox/index.ts b/examples/sandbox/index.ts
index c29b46a01..9d12acdaa 100644
--- a/examples/sandbox/index.ts
+++ b/examples/sandbox/index.ts
@@ -9,6 +9,7 @@ import * as ledgerWebUSB from "@shapeshiftoss/hdwallet-ledger-webusb";
import * as metaMask from "@shapeshiftoss/hdwallet-metamask";
import * as native from "@shapeshiftoss/hdwallet-native";
import * as portis from "@shapeshiftoss/hdwallet-portis";
+import * as tallyHo from "@shapeshiftoss/hdwallet-tallyho";
import * as trezorConnect from "@shapeshiftoss/hdwallet-trezor-connect";
import * as xdefi from "@shapeshiftoss/hdwallet-xdefi";
import $ from "jquery";
@@ -64,6 +65,7 @@ const kkbridgeAdapter = keepkeyTcp.TCPKeepKeyAdapter.useKeyring(keyring);
const kkemuAdapter = keepkeyTcp.TCPKeepKeyAdapter.useKeyring(keyring);
const portisAdapter = portis.PortisAdapter.useKeyring(keyring, { portisAppId });
const metaMaskAdapter = metaMask.MetaMaskAdapter.useKeyring(keyring);
+const tallyHoAdapter = tallyHo.TallyHoAdapter.useKeyring(keyring);
const xdefiAdapter = xdefi.XDEFIAdapter.useKeyring(keyring);
const nativeAdapter = native.NativeAdapter.useKeyring(keyring, {
mnemonic,
@@ -95,6 +97,7 @@ const $ledgerwebhid = $("#ledgerwebhid");
const $portis = $("#portis");
const $native = $("#native");
const $metaMask = $("#metaMask");
+const $tallyHo = $("#tallyHo");
const $xdefi = $("#xdefi");
const $keyring = $("#keyring");
@@ -181,6 +184,20 @@ $metaMask.on("click", async (e) => {
console.error(error);
}
});
+
+$tallyHo.on("click", async (e) => {
+ e.preventDefault();
+ wallet = await tallyHoAdapter.pairDevice();
+ window["wallet"] = wallet;
+ let deviceID = "nothing";
+ try {
+ deviceID = await wallet.getDeviceID();
+ $("#keyring select").val(deviceID);
+ } catch (error) {
+ console.error(error);
+ }
+});
+
$xdefi.on("click", async (e) => {
e.preventDefault();
wallet = await xdefiAdapter.pairDevice("testid");
@@ -275,6 +292,12 @@ async function deviceConnected(deviceId) {
console.error("Could not initialize MetaMaskAdapter", e);
}
+ try {
+ await tallyHoAdapter.initialize();
+ } catch (e) {
+ console.error("Could not initialize TallyHoAdapter", e);
+ }
+
for (const deviceID of Object.keys(keyring.wallets)) {
await deviceConnected(deviceID);
}
diff --git a/integration/src/integration.ts b/integration/src/integration.ts
index 96edfb964..d0d48f0cd 100644
--- a/integration/src/integration.ts
+++ b/integration/src/integration.ts
@@ -4,6 +4,7 @@ import * as ledger from "@shapeshiftoss/hdwallet-ledger";
import * as metamask from "@shapeshiftoss/hdwallet-metamask";
import * as native from "@shapeshiftoss/hdwallet-native";
import * as portis from "@shapeshiftoss/hdwallet-portis";
+import * as tallyHo from "@shapeshiftoss/hdwallet-tallyho";
import * as trezor from "@shapeshiftoss/hdwallet-trezor";
import * as xdefi from "@shapeshiftoss/hdwallet-xdefi";
@@ -54,6 +55,7 @@ export function integration(suite: WalletSuite): void {
(portis.isPortis(wallet) ? 1 : 0) +
(native.isNative(wallet) ? 1 : 0) +
(metamask.isMetaMask(wallet) ? 1 : 0) +
+ (tallyHo.isTallyHo(wallet) ? 1 : 0) +
(xdefi.isXDEFI(wallet) ? 1 : 0)
).toEqual(1);
});
diff --git a/integration/src/wallets/tallyho.ts b/integration/src/wallets/tallyho.ts
new file mode 100644
index 000000000..cb8f6530f
--- /dev/null
+++ b/integration/src/wallets/tallyho.ts
@@ -0,0 +1,166 @@
+import * as core from "@shapeshiftoss/hdwallet-core";
+import * as tallyHo from "@shapeshiftoss/hdwallet-tallyho";
+
+export function name(): string {
+ return "Tally Ho";
+}
+
+export function createInfo(): core.HDWalletInfo {
+ return new tallyHo.TallyHoHDWalletInfo();
+}
+
+export async function createWallet(): Promise {
+ const provider = {
+ request: jest.fn(({ method, params }: any) => {
+ switch (method) {
+ case "eth_accounts":
+ return ["0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8"];
+ case "personal_sign": {
+ const [message] = params;
+
+ if (message === "48656c6c6f20576f726c64")
+ return "0x29f7212ecc1c76cea81174af267b67506f754ea8c73f144afa900a0d85b24b21319621aeb062903e856352f38305710190869c3ce5a1425d65ef4fa558d0fc251b";
+
+ throw new Error("unknown message");
+ }
+ case "eth_sendTransaction": {
+ const [{ to }] = params;
+
+ return `txHash-${to}`;
+ }
+ default:
+ throw new Error(`ethereum: Unknown method ${method}`);
+ }
+ }),
+ };
+ const wallet = new tallyHo.TallyHoHDWallet(provider);
+ await wallet.initialize();
+ return wallet;
+}
+
+export function selfTest(get: () => core.HDWallet): void {
+ let wallet: tallyHo.TallyHoHDWallet;
+
+ beforeAll(async () => {
+ const w = get() as tallyHo.TallyHoHDWallet;
+
+ if (tallyHo.isTallyHo(w) && !core.supportsBTC(w) && core.supportsETH(w)) {
+ wallet = w;
+ } else {
+ throw "Wallet is not a Tally";
+ }
+ });
+
+ it("supports Ethereum mainnet", async () => {
+ if (!wallet) return;
+ expect(await wallet.ethSupportsNetwork()).toEqual(true);
+ });
+
+ it("does not support BTC", async () => {
+ if (!wallet) return;
+ expect(core.supportsBTC(wallet)).toBe(false);
+ });
+
+ it("does not support Native ShapeShift", async () => {
+ if (!wallet) return;
+ expect(wallet.ethSupportsNativeShapeShift()).toEqual(false);
+ });
+
+ it("does support EIP1559", async () => {
+ if (!wallet) return;
+ expect(await wallet.ethSupportsEIP1559()).toEqual(true);
+ });
+
+ it("does not support Secure Transfer", async () => {
+ if (!wallet) return;
+ expect(await wallet.ethSupportsSecureTransfer()).toEqual(false);
+ });
+
+ it("uses correct eth bip44 paths", () => {
+ if (!wallet) return;
+ [0, 1, 3, 27].forEach((account) => {
+ const paths = wallet.ethGetAccountPaths({
+ coin: "Ethereum",
+ accountIdx: account,
+ });
+ expect(paths).toEqual([
+ {
+ addressNList: core.bip32ToAddressNList(`m/44'/60'/${account}'/0/0`),
+ hardenedPath: core.bip32ToAddressNList(`m/44'/60'/${account}'`),
+ relPath: [0, 0],
+ description: "TallyHo",
+ },
+ ]);
+ paths.forEach((path) => {
+ expect(
+ wallet.describePath({
+ coin: "Ethereum",
+ path: path.addressNList,
+ }).isKnown
+ ).toBeTruthy();
+ });
+ });
+ });
+
+ it("can describe ETH paths", () => {
+ if (!wallet) return;
+ expect(
+ wallet.describePath({
+ path: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ coin: "Ethereum",
+ })
+ ).toEqual({
+ verbose: "Ethereum Account #0",
+ coin: "Ethereum",
+ isKnown: true,
+ accountIdx: 0,
+ wholeAccount: true,
+ });
+
+ expect(
+ wallet.describePath({
+ path: core.bip32ToAddressNList("m/44'/60'/3'/0/0"),
+ coin: "Ethereum",
+ })
+ ).toEqual({
+ verbose: "Ethereum Account #3",
+ coin: "Ethereum",
+ isKnown: true,
+ accountIdx: 3,
+ wholeAccount: true,
+ });
+
+ expect(
+ wallet.describePath({
+ path: core.bip32ToAddressNList("m/44'/60'/0'/0/3"),
+ coin: "Ethereum",
+ })
+ ).toEqual({
+ verbose: "m/44'/60'/0'/0/3",
+ coin: "Ethereum",
+ isKnown: false,
+ });
+ });
+
+ it("should return a valid ETH address", async () => {
+ if (!wallet) return;
+ expect(
+ await wallet.ethGetAddress({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ showDisplay: false,
+ })
+ ).toEqual("0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8");
+ });
+
+ it("should sign a message", async () => {
+ if (!wallet) return;
+ const res = await wallet.ethSignMessage({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ message: "Hello World",
+ });
+ expect(res?.address).toEqual("0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8");
+ expect(res?.signature).toEqual(
+ "0x29f7212ecc1c76cea81174af267b67506f754ea8c73f144afa900a0d85b24b21319621aeb062903e856352f38305710190869c3ce5a1425d65ef4fa558d0fc251b"
+ );
+ });
+}
diff --git a/packages/hdwallet-tallyho/package.json b/packages/hdwallet-tallyho/package.json
new file mode 100644
index 000000000..5bd21d6a0
--- /dev/null
+++ b/packages/hdwallet-tallyho/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@shapeshiftoss/hdwallet-tallyho",
+ "version": "1.19.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "main": "dist/index.js",
+ "source": "src/index.ts",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsc --build",
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
+ "prepublishOnly": "yarn clean && yarn build"
+ },
+ "dependencies": {
+ "@shapeshiftoss/hdwallet-core": "1.19.0",
+ "lodash": "^4.17.21",
+ "tallyho-onboarding": "^1.0.2"
+ },
+ "devDependencies": {
+ "@types/lodash": "^4.14.168",
+ "typescript": "^4.3.2"
+ }
+}
diff --git a/packages/hdwallet-tallyho/src/adapter.test.ts b/packages/hdwallet-tallyho/src/adapter.test.ts
new file mode 100644
index 000000000..29c1c0f85
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/adapter.test.ts
@@ -0,0 +1,11 @@
+import * as core from "@shapeshiftoss/hdwallet-core";
+
+import { TallyHoAdapter } from "./adapter";
+
+describe("TallyHoAdapter", () => {
+ it("throws error if provider is not preset", async () => {
+ const keyring = new core.Keyring();
+ const adapter = TallyHoAdapter.useKeyring(keyring);
+ await expect(async () => await adapter.pairDevice()).rejects.toThrowError("Could not get Tally Ho accounts.");
+ });
+});
diff --git a/packages/hdwallet-tallyho/src/adapter.ts b/packages/hdwallet-tallyho/src/adapter.ts
new file mode 100644
index 000000000..a60b15c90
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/adapter.ts
@@ -0,0 +1,99 @@
+import * as core from "@shapeshiftoss/hdwallet-core";
+import TallyHoOnboarding from "tallyho-onboarding";
+
+import { TallyHoHDWallet } from "./tallyho";
+
+interface TallyHoEthereumProvider {
+ isTally?: boolean;
+}
+
+interface Window {
+ ethereum?: TallyHoEthereumProvider;
+}
+
+export class TallyHoAdapter {
+ keyring: core.Keyring;
+
+ private constructor(keyring: core.Keyring) {
+ this.keyring = keyring;
+ }
+
+ public static useKeyring(keyring: core.Keyring) {
+ return new TallyHoAdapter(keyring);
+ }
+
+ public async pairDevice(): Promise {
+ let provider: any;
+ // eslint-disable-next-line no-useless-catch
+ try {
+ provider = await this.detectTallyProvider();
+ } catch (error) {
+ throw error;
+ }
+ if (!provider) {
+ const onboarding = new TallyHoOnboarding();
+ onboarding.startOnboarding();
+ console.error("Please install Tally Ho!");
+ }
+ if (provider === null) {
+ throw new Error("Could not get Tally Ho accounts.");
+ }
+
+ // eslint-disable-next-line no-useless-catch
+ try {
+ await provider.request({ method: "eth_requestAccounts" });
+ } catch (error) {
+ throw error;
+ }
+ const wallet = new TallyHoHDWallet(provider);
+ const deviceID = await wallet.getDeviceID();
+ this.keyring.add(wallet, deviceID);
+ this.keyring.emit(["Tally Ho", deviceID, core.Events.CONNECT], deviceID);
+
+ return wallet;
+ }
+
+ /*
+ * Tally works the same way as metamask.
+ * This code is copied from the @metamask/detect-provider package
+ * @see https://www.npmjs.com/package/@metamask/detect-provider
+ */
+ private async detectTallyProvider(): Promise {
+ let handled = false;
+
+ return new Promise((resolve) => {
+ if ((window as Window).ethereum) {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ handleEthereum();
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ window.addEventListener("ethereum#initialized", handleEthereum, { once: true });
+
+ setTimeout(() => {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ handleEthereum();
+ }, 3000);
+ }
+
+ function handleEthereum() {
+ if (handled) {
+ return;
+ }
+ handled = true;
+
+ window.removeEventListener("ethereum#initialized", handleEthereum);
+
+ const { ethereum } = window as Window;
+
+ if (ethereum && ethereum.isTally) {
+ resolve(ethereum as unknown as TallyHoEthereumProvider);
+ } else {
+ const message = ethereum ? "Non-TallyHo window.ethereum detected." : "Unable to detect window.ethereum.";
+
+ console.error("hdwallet-tallyho: ", message);
+ resolve(null);
+ }
+ }
+ });
+ }
+}
diff --git a/packages/hdwallet-tallyho/src/ethereum.test.ts b/packages/hdwallet-tallyho/src/ethereum.test.ts
new file mode 100644
index 000000000..6a0c3ca62
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/ethereum.test.ts
@@ -0,0 +1,206 @@
+import * as core from "@shapeshiftoss/hdwallet-core";
+
+import * as ethereum from "./ethereum";
+
+describe("Tally Ho - Ethereum Adapter", () => {
+ it("ethVerifyMessage returns null as its not implemented", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockReturnValue("0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8"),
+ };
+ expect(
+ await ethereum.ethVerifyMessage(
+ {
+ address: "0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8",
+ message: "hello world",
+ signature:
+ "0x29f7212ecc1c76cea81174af267b67506f754ea8c73f144afa900a0d85b24b21319621aeb062903e856352f38305710190869c3ce5a1425d65ef4fa558d0fc251b",
+ },
+ ethereumProvider
+ )
+ ).toBe(null);
+ });
+ it("ethGetAccountPaths should return correct paths", () => {
+ const paths = ethereum.ethGetAccountPaths({ coin: "Ethereum", accountIdx: 0 });
+ expect(paths).toMatchObject([
+ {
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ hardenedPath: core.bip32ToAddressNList("m/44'/60'/0'"),
+ relPath: [0, 0],
+ description: "Tally Ho",
+ },
+ ]);
+ });
+ it("ethGetAccountPaths should return empty path", () => {
+ const paths = ethereum.ethGetAccountPaths({ coin: "RandomCoin", accountIdx: 0 });
+ expect(paths).toMatchObject([]);
+ });
+ it("ethSignTx returns null as its not implemented", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockReturnValue({
+ r: "0x63db3dd3bf3e1fe7dde1969c0fc8850e34116d0b501c0483a0e08c0f77b8ce0a",
+ s: "0x28297d012cccf389f6332415e96ee3fc0bbf8474d05f646e029cd281a031464b",
+ v: 38,
+ serialized:
+ "0xf86b018501dcd650008256229412ec06288edd7ae2cc41a843fe089237fc7354f0872c68af0bb140008026a063db3dd3bf3e1fe7dde1969c0fc8850e34116d0b501c0483a0e08c0f77b8ce0aa028297d012cccf389f6332415e96ee3fc0bbf8474d05f646e029cd281a031464b",
+ }),
+ };
+ expect(
+ await ethereum.ethSignTx(
+ {
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ },
+ ethereumProvider,
+ "0x123"
+ )
+ ).toEqual(null);
+ });
+ it("ethSendTx returns a valid hash", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockReturnValue("0x123"),
+ };
+
+ const hash = await ethereum.ethSendTx(
+ {
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ },
+ ethereumProvider,
+ "0x123"
+ );
+ expect(ethereumProvider.request).toHaveBeenCalled();
+ expect(hash).toMatchObject({ hash: "0x123" });
+ });
+ it("ethSendTx returns a valid hash if maxFeePerGas is present in msg", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockReturnValue("0x123"),
+ };
+
+ const hash = await ethereum.ethSendTx(
+ {
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ maxFeePerGas: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ },
+ ethereumProvider,
+ "0x123"
+ );
+ expect(ethereumProvider.request).toHaveBeenCalled();
+ expect(hash).toMatchObject({ hash: "0x123" });
+ });
+ it("ethSendTx returns null on error", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockRejectedValue(new Error("An Error has occurred")),
+ };
+
+ const hash = await ethereum.ethSendTx(
+ {
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ },
+ ethereumProvider,
+ "0x123"
+ );
+ expect(ethereumProvider.request).toHaveBeenCalled();
+ expect(hash).toBe(null);
+ });
+
+ it("ethSignMessage returns a valid signature object", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockReturnValue(
+ `Object {
+ "address": "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8",
+ "signature": "0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b",
+ }`
+ ),
+ };
+
+ const msg = "super secret message";
+ const sig = await ethereum.ethSignMessage(
+ {
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ message: msg,
+ },
+ ethereumProvider,
+ "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"
+ );
+
+ expect(sig).toMatchInlineSnapshot(`
+ Object {
+ "address": "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8",
+ "signature": "Object {
+ \\"address\\": \\"0x73d0385F4d8E00C5e6504C6030F47BF6212736A8\\",
+ \\"signature\\": \\"0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b\\",
+ }",
+ }
+ `);
+ });
+
+ it("ethSignMessage returns null on error", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockRejectedValue(new Error("An Error has occurred")),
+ };
+
+ const msg = "super secret message";
+ const sig = await ethereum.ethSignMessage(
+ {
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ message: msg,
+ },
+ ethereumProvider,
+ "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"
+ );
+
+ expect(sig).toBe(null);
+ });
+
+ it("ethGetAddress returns a valid address", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockReturnValue(["0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"]),
+ };
+
+ const address = await ethereum.ethGetAddress(ethereumProvider);
+
+ expect(address).toBe("0x73d0385F4d8E00C5e6504C6030F47BF6212736A8");
+ });
+ it("ethGetAddress returns null on error", async () => {
+ const ethereumProvider = {
+ request: jest.fn().mockRejectedValue(new Error("An error has occurred")),
+ };
+
+ const address = await ethereum.ethGetAddress(ethereumProvider);
+
+ expect(address).toBe(null);
+ });
+ it("ethGetAddress returns null if no provider", async () => {
+ const ethereumProvider = {};
+
+ const address = await ethereum.ethGetAddress(ethereumProvider);
+
+ expect(address).toBe(null);
+ });
+});
diff --git a/packages/hdwallet-tallyho/src/ethereum.ts b/packages/hdwallet-tallyho/src/ethereum.ts
new file mode 100644
index 000000000..c579e5c96
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/ethereum.ts
@@ -0,0 +1,130 @@
+import * as core from "@shapeshiftoss/hdwallet-core";
+
+export function describeETHPath(path: core.BIP32Path): core.PathDescription {
+ const pathStr = core.addressNListToBIP32(path);
+ const unknown: core.PathDescription = {
+ verbose: pathStr,
+ coin: "Ethereum",
+ isKnown: false,
+ };
+
+ if (path.length !== 5) return unknown;
+
+ if (path[0] !== 0x80000000 + 44) return unknown;
+
+ if (path[1] !== 0x80000000 + core.slip44ByCoin("Ethereum")) return unknown;
+
+ if ((path[2] & 0x80000000) >>> 0 !== 0x80000000) return unknown;
+
+ if (path[3] !== 0) return unknown;
+
+ if (path[4] !== 0) return unknown;
+
+ const index = path[2] & 0x7fffffff;
+ return {
+ verbose: `Ethereum Account #${index}`,
+ accountIdx: index,
+ wholeAccount: true,
+ coin: "Ethereum",
+ isKnown: true,
+ };
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export async function ethVerifyMessage(msg: core.ETHVerifyMessage, ethereum: any): Promise {
+ console.error("Method ethVerifyMessage unsupported for Tally Ho wallet!");
+ return null;
+}
+
+export function ethGetAccountPaths(msg: core.ETHGetAccountPath): Array {
+ const slip44 = core.slip44ByCoin(msg.coin);
+ if (slip44 === undefined) return [];
+ return [
+ {
+ addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx, 0, 0],
+ hardenedPath: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx],
+ relPath: [0, 0],
+ description: "Tally Ho",
+ },
+ ];
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export async function ethSignTx(msg: core.ETHSignTx, ethereum: any, from: string): Promise {
+ console.error("Method ethSignTx unsupported for Tally Ho wallet!");
+ return null;
+}
+
+export async function ethSendTx(msg: core.ETHSignTx, ethereum: any, from: string): Promise {
+ try {
+ const utxBase = {
+ from: from,
+ to: msg.to,
+ value: msg.value,
+ data: msg.data,
+ chainId: msg.chainId,
+ nonce: msg.nonce,
+ // Tally Ho, like other Web3 libraries, derives its transaction schema from Ethereum's official JSON-RPC API specification
+ // (https://github.com/ethereum/execution-apis/blob/d63d2a02bcd2a8cef54ae2fc5bbff8b4fac944eb/src/schemas/transaction.json).
+ // That schema defines the use of the label `gas` to set the transaction's gas limit and not `gasLimit` as used in other
+ // libraries and as stated in the official yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf).
+ gas: msg.gasLimit,
+ };
+
+ const utx = msg.maxFeePerGas
+ ? {
+ ...utxBase,
+ maxFeePerGas: msg.maxFeePerGas,
+ maxPriorityFeePerGas: msg.maxPriorityFeePerGas,
+ }
+ : { ...utxBase, gasPrice: msg.gasPrice };
+
+ const signedTx = await ethereum.request({
+ method: "eth_sendTransaction",
+ params: [utx],
+ });
+
+ return {
+ hash: signedTx,
+ } as core.ETHTxHash;
+ } catch (error) {
+ console.error(error);
+ return null;
+ }
+}
+
+export async function ethSignMessage(
+ msg: core.ETHSignMessage,
+ ethereum: any,
+ address: string
+): Promise {
+ try {
+ const signedMsg = await ethereum.request({
+ method: "personal_sign",
+ params: [Buffer.from(msg.message).toString("hex"), address],
+ });
+
+ return {
+ address: address,
+ signature: signedMsg,
+ } as core.ETHSignedMessage;
+ } catch (error) {
+ console.error(error);
+ return null;
+ }
+}
+
+export async function ethGetAddress(ethereum: any): Promise {
+ if (!(ethereum && ethereum.request)) {
+ return null;
+ }
+ try {
+ const ethAccounts = await ethereum.request({
+ method: "eth_accounts",
+ });
+ return ethAccounts[0];
+ } catch (error) {
+ console.error(error);
+ return null;
+ }
+}
diff --git a/packages/hdwallet-tallyho/src/index.test.ts b/packages/hdwallet-tallyho/src/index.test.ts
new file mode 100644
index 000000000..7913ea523
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/index.test.ts
@@ -0,0 +1,11 @@
+import * as library from "./";
+
+describe("Exports all expected classes", () => {
+ it("should export TallyHoAdapter", () => {
+ expect(library.TallyHoAdapter.name).toBe("TallyHoAdapter");
+ });
+
+ it("should export XDeFiHDWallet", () => {
+ expect(library.TallyHoHDWallet.name).toBe("TallyHoHDWallet");
+ });
+});
diff --git a/packages/hdwallet-tallyho/src/index.ts b/packages/hdwallet-tallyho/src/index.ts
new file mode 100644
index 000000000..db2aa34ab
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/index.ts
@@ -0,0 +1,2 @@
+export * from "./adapter";
+export * from "./tallyho";
diff --git a/packages/hdwallet-tallyho/src/tallyho.test.ts b/packages/hdwallet-tallyho/src/tallyho.test.ts
new file mode 100644
index 000000000..e14e34989
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/tallyho.test.ts
@@ -0,0 +1,203 @@
+import * as core from "@shapeshiftoss/hdwallet-core";
+
+import { TallyHoHDWallet, TallyHoHDWalletInfo } from ".";
+
+describe("HDWalletInfo", () => {
+ const info = new TallyHoHDWalletInfo();
+
+ it("should have correct metadata", async () => {
+ expect(info.getVendor()).toBe("Tally Ho");
+ expect(info.hasOnDevicePinEntry()).toBe(false);
+ expect(info.hasOnDevicePassphrase()).toBe(true);
+ expect(info.hasOnDeviceDisplay()).toBe(true);
+ expect(info.hasOnDeviceRecovery()).toBe(true);
+ expect(await info.ethSupportsNetwork(1)).toBe(true);
+ expect(await info.ethSupportsSecureTransfer()).toBe(false);
+ expect(info.ethSupportsNativeShapeShift()).toBe(false);
+ expect(await info.ethSupportsEIP1559()).toBe(true);
+ expect(await info.supportsOfflineSigning()).toBe(false);
+ expect(await info.supportsBroadcast()).toBe(true);
+ });
+ it("should produce correct path descriptions", () => {
+ expect(info.hasNativeShapeShift()).toBe(false);
+ [
+ {
+ msg: { coin: "Ethereum", path: [44 + 0x80000000, 60 + 0x80000000, 0 + 0x80000000, 0, 0] },
+ out: { coin: "Ethereum", verbose: "Ethereum Account #0", isKnown: true },
+ },
+ ].forEach((x) => expect(info.describePath(x.msg)).toMatchObject(x.out));
+ expect(() => info.describePath({ coin: "foobar", path: [1, 2, 3] })).toThrowError("Unsupported path");
+ });
+});
+
+describe("TallyHoHDWallet", () => {
+ let wallet: TallyHoHDWallet;
+ beforeEach(() => {
+ wallet = new TallyHoHDWallet(core.untouchable("TallyHoHDWallet:provider"));
+ wallet.ethAddress = "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8";
+ });
+
+ it("should match the metadata", async () => {
+ expect(wallet.getVendor()).toBe("Tally Ho");
+ expect(wallet.hasOnDevicePinEntry()).toBe(false);
+ expect(wallet.hasOnDevicePassphrase()).toBe(true);
+ expect(wallet.hasOnDeviceDisplay()).toBe(true);
+ expect(wallet.hasOnDeviceRecovery()).toBe(true);
+ expect(await wallet.ethSupportsNetwork(1)).toBe(true);
+ expect(await wallet.ethSupportsSecureTransfer()).toBe(false);
+ expect(wallet.ethSupportsNativeShapeShift()).toBe(false);
+ expect(await wallet.ethSupportsEIP1559()).toBe(true);
+ expect(await wallet.supportsOfflineSigning()).toBe(false);
+ expect(await wallet.supportsBroadcast()).toBe(true);
+ });
+
+ it("should test ethSignTx", async () => {
+ wallet.ethAddress = "0x123";
+ wallet.provider = {
+ request: jest.fn().mockReturnValue({
+ r: "0x63db3dd3bf3e1fe7dde1969c0fc8850e34116d0b501c0483a0e08c0f77b8ce0a",
+ s: "0x28297d012cccf389f6332415e96ee3fc0bbf8474d05f646e029cd281a031464b",
+ v: 38,
+ serialized:
+ "0xf86b018501dcd650008256229412ec06288edd7ae2cc41a843fe089237fc7354f0872c68af0bb140008026a063db3dd3bf3e1fe7dde1969c0fc8850e34116d0b501c0483a0e08c0f77b8ce0aa028297d012cccf389f6332415e96ee3fc0bbf8474d05f646e029cd281a031464b",
+ }),
+ };
+ expect(
+ await wallet.ethSignTx({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ })
+ ).toEqual(null);
+ });
+
+ it("should test ethSignMessage", async () => {
+ wallet.provider = {
+ request: jest.fn().mockReturnValue(
+ `Object {
+ "address": "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8",
+ "signature": "0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b",
+ }`
+ ),
+ };
+ const msg = "super secret message";
+ expect(
+ await wallet.ethSignMessage({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ message: msg,
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "address": "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8",
+ "signature": "Object {
+ \\"address\\": \\"0x73d0385F4d8E00C5e6504C6030F47BF6212736A8\\",
+ \\"signature\\": \\"0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b\\",
+ }",
+ }
+ `);
+ });
+
+ it("ethSignMessage returns null on error", async () => {
+ wallet.provider = {
+ request: jest.fn().mockRejectedValue(new Error("An Error has occurred")),
+ };
+
+ const msg = "super secret message";
+ const sig = await wallet.ethSignMessage({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ message: msg,
+ });
+
+ expect(sig).toBe(null);
+ });
+
+ it("ethGetAddress returns a valid address", async () => {
+ wallet.provider = {
+ request: jest.fn().mockReturnValue(["0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"]),
+ };
+
+ const msg = "super secret message";
+ const sig = await wallet.ethSignMessage({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ message: msg,
+ });
+
+ expect(sig).toMatchObject({
+ address: "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8",
+ signature: ["0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"],
+ });
+ });
+ it("ethSendTx returns a valid hash", async () => {
+ wallet.provider = {
+ request: jest.fn().mockReturnValue("0x123"),
+ };
+
+ const hash = await wallet.ethSendTx({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ });
+ expect(wallet.provider.request).toHaveBeenCalled();
+ expect(hash).toMatchObject({ hash: "0x123" });
+ });
+ it("ethSendTx returns a valid hash if maxFeePerGas is present in msg", async () => {
+ wallet.provider = {
+ request: jest.fn().mockReturnValue("0x123"),
+ };
+
+ const hash = await wallet.ethSendTx({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ maxFeePerGas: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ });
+ expect(wallet.provider.request).toHaveBeenCalled();
+ expect(hash).toMatchObject({ hash: "0x123" });
+ });
+ it("ethSendTx returns null on error", async () => {
+ wallet.provider = {
+ request: jest.fn().mockRejectedValue(new Error("An Error has occurred")),
+ };
+
+ const hash = await wallet.ethSendTx({
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
+ nonce: "0xDEADBEEF",
+ gasPrice: "0xDEADBEEF",
+ gasLimit: "0xDEADBEEF",
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
+ chainId: 1,
+ });
+ expect(wallet.provider.request).toHaveBeenCalled();
+ expect(hash).toBe(null);
+ });
+ it("ethVerifyMessage returns null as its not implemented", async () => {
+ wallet.provider = {
+ request: jest.fn().mockReturnValue("0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8"),
+ };
+ expect(
+ await wallet.ethVerifyMessage({
+ address: "0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8",
+ message: "hello world",
+ signature:
+ "0x29f7212ecc1c76cea81174af267b67506f754ea8c73f144afa900a0d85b24b21319621aeb062903e856352f38305710190869c3ce5a1425d65ef4fa558d0fc251b",
+ })
+ ).toEqual(null);
+ });
+});
diff --git a/packages/hdwallet-tallyho/src/tallyho.ts b/packages/hdwallet-tallyho/src/tallyho.ts
new file mode 100644
index 000000000..6b80f6f05
--- /dev/null
+++ b/packages/hdwallet-tallyho/src/tallyho.ts
@@ -0,0 +1,269 @@
+import * as core 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?
+ 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 {
+ 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 _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 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 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 {
+ 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";
+ }
+}
diff --git a/packages/hdwallet-tallyho/tsconfig.json b/packages/hdwallet-tallyho/tsconfig.json
new file mode 100644
index 000000000..b9fea9e4f
--- /dev/null
+++ b/packages/hdwallet-tallyho/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"],
+ "references": [{ "path": "../hdwallet-core" }]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 09798d13b..af1c9a3e1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -58,6 +58,7 @@
{ "path": "./packages/hdwallet-native" },
{ "path": "./packages/hdwallet-native-vault" },
{ "path": "./packages/hdwallet-portis" },
+ { "path": "./packages/hdwallet-tallyho" },
{ "path": "./packages/hdwallet-trezor" },
{ "path": "./packages/hdwallet-trezor-connect" },
{ "path": "./packages/hdwallet-xdefi" }
diff --git a/yarn.lock b/yarn.lock
index fa28901df..220304949 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3887,6 +3887,16 @@
web-encoding "^1.1.0"
wif "^2.0.6"
+"@shapeshiftoss/hdwallet-core@1.19.0":
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/@shapeshiftoss/hdwallet-core/-/hdwallet-core-1.19.0.tgz#7eeae9be9f90fff53887611c6ac02c334781baa3"
+ integrity sha512-Dht99dolYQLDuunKsyQXuNcZH20MrgNrIcb4qG2XLrwuwMUsUOXguQ0MBxi6fFl81hz3G8481v83gOYA5L5tGw==
+ dependencies:
+ eventemitter2 "^5.0.1"
+ lodash "^4.17.21"
+ rxjs "^6.4.0"
+ type-assertions "^1.1.0"
+
"@shapeshiftoss/hdwallet-core@latest":
version "1.18.4"
resolved "https://registry.yarnpkg.com/@shapeshiftoss/hdwallet-core/-/hdwallet-core-1.18.4.tgz#7272baa4b43de0fbb5e651d47cdeb9554f4ddd33"
@@ -10676,10 +10686,10 @@ jest@^26.6.3:
import-local "^3.0.2"
jest-cli "^26.6.3"
-jose@^4.3.5:
- version "4.3.5"
- resolved "https://registry.yarnpkg.com/jose/-/jose-4.3.5.tgz#890ec0b3bf26db0b36946ca54087335200deb6f7"
- integrity sha512-mdTu3En79OYMGBNHw4828hl6ZUOb+gQtNZVRgM+eVL3Rrs4ZYUv/yHPpfDh65GN2HhKBvJsvA0/6tKEcpkyzeg==
+jose@^4.3.2:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/jose/-/jose-4.7.0.tgz#f0a88709285e1a6c1ba7df0d19e33f64aca03cad"
+ integrity sha512-DJNm2vlcVW+0Fhl5LVxdhXK5Nw5VYZvL79uiq3ZCRm6vqzTuEVT9T5lIRfzmkGbaaiV+edefGog25TWVyJ4mRQ==
jquery@^3.4.1:
version "3.6.0"
@@ -15189,6 +15199,13 @@ symbol-tree@^3.2.2, symbol-tree@^3.2.4:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+tallyho-onboarding@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/tallyho-onboarding/-/tallyho-onboarding-1.0.2.tgz#afc7dc4eb05b3a7861ead215e798585e1cbe2e91"
+ integrity sha512-bdFT/fNrFrq1BYVgjl/JKtwDmeS+z2u0415PoxmGmmYYRfdcKqXtEPImMoEbVwGtOeN0iFVohuS8ESrrAe+w7w==
+ dependencies:
+ bowser "^2.9.0"
+
tar@^4.0.2, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8:
version "4.4.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
@@ -15721,7 +15738,7 @@ typescript@^3.9.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
-typescript@^4.2.4, typescript@^4.3.2:
+typescript@^4.3.2:
version "4.3.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==