Skip to content

Commit

Permalink
Merge pull request #506 from 0xzoz/add-tally-wallet
Browse files Browse the repository at this point in the history
Add tally wallet
  • Loading branch information
0xdef1cafe authored May 12, 2022
2 parents 2cebeae + 067ac70 commit 84e453a
Show file tree
Hide file tree
Showing 16 changed files with 1,182 additions and 6 deletions.
3 changes: 2 additions & 1 deletion examples/sandbox/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ <h4>Select</h4>
<button id="portis">Pair Portis</button>
<button id="native">Pair Native</button>
<button id="metaMask">Pair MetaMask</button>
<button id="xdefi">Pair XDEFI</button>
<button id="tallyHo">Pair Tally Ho</button>
<button id="xdefi">Pair XDeFi</button>

<select id="keyring" style="height: 100px" size="4"></select>
</div>
Expand Down
23 changes: 23 additions & 0 deletions examples/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions integration/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
});
Expand Down
166 changes: 166 additions & 0 deletions integration/src/wallets/tallyho.ts
Original file line number Diff line number Diff line change
@@ -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<core.HDWallet> {
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"
);
});
}
25 changes: 25 additions & 0 deletions packages/hdwallet-tallyho/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
11 changes: 11 additions & 0 deletions packages/hdwallet-tallyho/src/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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.");
});
});
99 changes: 99 additions & 0 deletions packages/hdwallet-tallyho/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -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<TallyHoHDWallet> {
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<TallyHoEthereumProvider | null> {
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);
}
}
});
}
}
Loading

1 comment on commit 84e453a

@vercel
Copy link

@vercel vercel bot commented on 84e453a May 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

hdwallet – ./

hdwallet-git-master-shapeshift.vercel.app
hdwallet-shapeshift.vercel.app

Please sign in to comment.