diff --git a/README.md b/README.md index 9ea8139c..39da06ab 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,16 @@ inter-compatibility between keystores. Implementing features which increase complexity and reduce inter-compatibility should be discouraged. +### Developing locally with another app (ex. [Caravan](https://github.com/unchained-capital/caravan)) + +If you would like to develop unchained-wallets locally while also developing an app that depends on unchained-wallets, you'll need to do a few steps: + +1. Change `"main": "lib/index.js"` in package.json to `"main": "src/index.ts"` This step is temporary while we convert this package to ESM. +1. In the root of this project run `npm link` +1. In the root of the project that depends on this package run `npm link unchained-wallets` + +Now when you start up your app, whatever bundler you're using (Vite, Webpack, etc.) should compile this package as well. + ### Developing against local Trezor connect If for some reason you need to use a [local instance of Trezor Connect](https://wiki.trezor.io/Developers_guide:Running_Trezor_Connect_on_localhost) diff --git a/images/compile.js b/images/compile.js index 256a2443..c2b86eca 100644 --- a/images/compile.js +++ b/images/compile.js @@ -3,14 +3,13 @@ // // FIXME there is probably a smart babel-based way to do this. - -const fs = require('fs'); -const path = require('path'); -const imagesDir = path.dirname(__filename); +import { readFileSync, writeFileSync } from "fs"; +import { dirname, join } from "path"; +const imagesDir = dirname(__filename); function base64Data(imagePath) { - const bytes = fs.readFileSync(path.join(imagesDir, imagePath)); - return Buffer.from(bytes).toString('base64'); + const bytes = readFileSync(join(imagesDir, imagePath)); + return Buffer.from(bytes).toString("base64"); } function mimeType(imagePath) { @@ -33,21 +32,66 @@ function imageData(imagePath, label) { const IMAGES = { ledger: { - warning: imageData("ledger/warning.png", "Ledger screen displaying a `WARNING!` message."), - derivationPathIsUnusualV1: imageData("ledger/derivationPathIsUnusualV1.png" ,"Ledger screen displaying a message about an 'unusual path'."), - derivationPathV1: imageData("ledger/derivationPathV1.png", "Ledger screen displaying a derivation path."), - rejectIfNotSureV1: imageData("ledger/rejectIfNotSureV1.png", "Ledger screen displaying a prompt about being sure."), - addressScrollV1: imageData("ledger/addressScrollV1.png", "Ledger screen displaying a bitcoin address."), - unusualDerivationBeta: imageData("ledger/unusualDerivationBeta.png", "The derivation path is unusual."), - fullDerivationPathBeta: imageData("ledger/fullDerivationPathBeta.png", "Ledger screen displaying a derivation path."), - rejectIfNotSureBeta: imageData("ledger/rejectIfNotSureBeta.png", "Reject if you're not sure."), - approveDerivationBeta: imageData("ledger/approveDerivationBeta.png", "Approve the derivation path if you are sure."), - derivationPathBeta: imageData("ledger/derivationPathBeta.png", "Ledger screen displaying a derivation path."), - addressClickThroughBeta: imageData("ledger/addressClickThroughBeta.png", "Ledger screen will display your corresponding public key."), - approveAddressBeta: imageData("ledger/approveAddressBeta.png", "Ledger screen asks for your approval."), - exportPublicKeyBeta: imageData("ledger/exportPublicKeyBeta.png", "Ledger screen asking to export public key"), - exportPublicKeyV1: imageData("ledger/exportPublicKeyV1.png", "Ledger screen asking to export public key"), - } + warning: imageData( + "ledger/warning.png", + "Ledger screen displaying a `WARNING!` message." + ), + derivationPathIsUnusualV1: imageData( + "ledger/derivationPathIsUnusualV1.png", + "Ledger screen displaying a message about an 'unusual path'." + ), + derivationPathV1: imageData( + "ledger/derivationPathV1.png", + "Ledger screen displaying a derivation path." + ), + rejectIfNotSureV1: imageData( + "ledger/rejectIfNotSureV1.png", + "Ledger screen displaying a prompt about being sure." + ), + addressScrollV1: imageData( + "ledger/addressScrollV1.png", + "Ledger screen displaying a bitcoin address." + ), + unusualDerivationBeta: imageData( + "ledger/unusualDerivationBeta.png", + "The derivation path is unusual." + ), + fullDerivationPathBeta: imageData( + "ledger/fullDerivationPathBeta.png", + "Ledger screen displaying a derivation path." + ), + rejectIfNotSureBeta: imageData( + "ledger/rejectIfNotSureBeta.png", + "Reject if you're not sure." + ), + approveDerivationBeta: imageData( + "ledger/approveDerivationBeta.png", + "Approve the derivation path if you are sure." + ), + derivationPathBeta: imageData( + "ledger/derivationPathBeta.png", + "Ledger screen displaying a derivation path." + ), + addressClickThroughBeta: imageData( + "ledger/addressClickThroughBeta.png", + "Ledger screen will display your corresponding public key." + ), + approveAddressBeta: imageData( + "ledger/approveAddressBeta.png", + "Ledger screen asks for your approval." + ), + exportPublicKeyBeta: imageData( + "ledger/exportPublicKeyBeta.png", + "Ledger screen asking to export public key" + ), + exportPublicKeyV1: imageData( + "ledger/exportPublicKeyV1.png", + "Ledger screen asking to export public key" + ), + }, }; -fs.writeFileSync(path.join(imagesDir, '../lib/images.js'), "export default " + JSON.stringify(IMAGES) + ";"); +writeFileSync( + join(imagesDir, "../lib/images.js"), + `export default ${JSON.stringify(IMAGES)};` +); diff --git a/package.json b/package.json index 2ab8f680..ff3a1ca1 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "build-types": "tsc -p tsconfig.json", "build-types:watch": "tsc --watch", "check-types": "tsc -p tsconfig.json --noEmit --emitDeclarationOnly false", - "compile": "babel --extensions .ts,.js -d lib/ src/ && npm run compile-images", + "compile": "babel --extensions .ts,.js -d lib/ src/", "compile:trezor-dev": "TREZOR_DEV=true babel --extensions .ts,.js --watch -d lib/ src/ && npm run compile-images", "compile:watch": "babel --watch --extensions .ts,.js -d lib/ src/ && compile-images", "prepublish": "npm run compile", diff --git a/src/index.ts b/src/index.ts index 550c17d0..bf8406ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -610,4 +610,7 @@ export * from "./custom"; export * from "./hermit"; export * from "./ledger"; export * from "./trezor"; -export * from "./policy"; +import * as MultisigWallet from "./policy"; + +const MultisigWalletPolicy = MultisigWallet.MultisigWalletPolicy; +export { braidDetailsToWalletConfig, MultisigWalletPolicy }; diff --git a/src/ledger.ts b/src/ledger.ts index a60174a1..76b19cf3 100644 --- a/src/ledger.ts +++ b/src/ledger.ts @@ -72,8 +72,7 @@ export const LEDGER = "ledger"; export const LEDGER_V2 = "ledger_v2"; -/* eslint-disable @typescript-eslint/no-var-requires */ -const TransportU2F = require("@ledgerhq/hw-transport-u2f").default; +import TransportU2F from "@ledgerhq/hw-transport-u2f"; import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; import LedgerBtc from "@ledgerhq/hw-app-btc"; diff --git a/src/trezor.js b/src/trezor.js index 545a2725..5f2c3114 100644 --- a/src/trezor.js +++ b/src/trezor.js @@ -61,8 +61,7 @@ import { MULTISIG_ROOT } from "./index"; */ export const TREZOR = "trezor"; -// eslint-disable-next-line -const TrezorConnect = require("@trezor/connect-web").default; +import TrezorConnect from "@trezor/connect-web"; const ADDRESS_SCRIPT_TYPES = { [P2SH]: "SPENDMULTISIG", diff --git a/src/trezor.test.js b/src/trezor.test.js index d2e5ed9f..bf70a3d6 100644 --- a/src/trezor.test.js +++ b/src/trezor.test.js @@ -6,14 +6,10 @@ import { MAINNET, ROOT_FINGERPRINT, TEST_FIXTURES, - PSBT_MAGIC_B64, networkData, + PSBT_MAGIC_B64, + networkData, } from "unchained-bitcoin"; -import { - PENDING, - ACTIVE, - INFO, - ERROR, -} from "./interaction"; +import { PENDING, ACTIVE, INFO, ERROR } from "./interaction"; import { trezorCoin, @@ -26,55 +22,57 @@ import { TrezorConfirmMultisigAddress, TrezorSignMessage, } from "./trezor"; -import {ECPair, payments} from "bitcoinjs-lib"; +import { ECPair, payments } from "bitcoinjs-lib"; -const TrezorConnect = require("@trezor/connect-web").default; +import TrezorConnect from "@trezor/connect-web"; function itHasStandardMessages(interactionBuilder) { it("has a message about ensuring your device is plugged in", () => { - expect(interactionBuilder().hasMessagesFor({ - state: PENDING, - level: INFO, - code: "device.connect", - text: "plugged in", - })).toBe(true); + expect( + interactionBuilder().hasMessagesFor({ + state: PENDING, + level: INFO, + code: "device.connect", + text: "plugged in", + }) + ).toBe(true); }); it("has a message about the TrezorConnect popup and enabling popups", () => { - expect(interactionBuilder().hasMessagesFor({ - state: ACTIVE, - level: INFO, - code: "trezor.connect.generic", - text: "enabled popups", - })).toBe(true); + expect( + interactionBuilder().hasMessagesFor({ + state: ACTIVE, + level: INFO, + code: "trezor.connect.generic", + text: "enabled popups", + }) + ).toBe(true); }); } function itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder) { - it("throws an error on an unsuccessful request", async () => { const interaction = interactionBuilder(); - interaction.connectParams = () => ([ + interaction.connectParams = () => [ () => ({ success: false, - payload: {error: "foobar"}, - }), {}, - ]); + payload: { error: "foobar" }, + }), + {}, + ]; try { await interaction.run(); } catch (e) { expect(e.message).toMatch(/foobar/i); } }); - } - -describe('trezor', () => { - - describe('TrezorInteraction', () => { - - function interactionBuilder() { return new TrezorInteraction({network: MAINNET}); } +describe("trezor", () => { + describe("TrezorInteraction", () => { + function interactionBuilder() { + return new TrezorInteraction({ network: MAINNET }); + } itHasStandardMessages(interactionBuilder); itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); @@ -86,12 +84,12 @@ describe('trezor', () => { expect(e.message).toMatch(/subclass of TrezorInteraction/i); } }); - }); describe("TrezorGetMetadata", () => { - - function interactionBuilder() { return new TrezorGetMetadata({network: MAINNET}); } + function interactionBuilder() { + return new TrezorGetMetadata({ network: MAINNET }); + } itHasStandardMessages(interactionBuilder); itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); @@ -126,7 +124,8 @@ describe('trezor', () => { revision: "ef8...862d7", unfinished_backup: null, vendor: "bitcointrezor.com", - })).toEqual({ + }) + ).toEqual({ spec: "Model 1 v.1.6.3 w/PIN", model: "Model 1", version: { @@ -147,11 +146,9 @@ describe('trezor', () => { expect(method).toEqual(TrezorConnect.getFeatures); expect(params).toEqual({}); }); - }); describe("TrezorExportHDNode", () => { - const bip32Path = "m/45'/0'/0'/0'"; function interactionBuilder() { @@ -164,29 +161,33 @@ describe('trezor', () => { itHasStandardMessages(interactionBuilder); itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); - it('constructor adds error message on invalid bip32path', () => { + it("constructor adds error message on invalid bip32path", () => { const interaction = new TrezorExportHDNode({ - bip32Path: 'm/foo', + bip32Path: "m/foo", network: MAINNET, }); - expect(interaction.hasMessagesFor({ - state: PENDING, - level: ERROR, - code: "trezor.bip32_path.path_error", - })).toBe(true); - }) + expect( + interaction.hasMessagesFor({ + state: PENDING, + level: ERROR, + code: "trezor.bip32_path.path_error", + }) + ).toBe(true); + }); - it('adds error message on bip32path { + it("adds error message on bip32path { const interaction = new TrezorExportHDNode({ - bip32Path: 'm/45', + bip32Path: "m/45", network: MAINNET, }); - expect(interaction.hasMessagesFor({ - state: PENDING, - level: ERROR, - code: "trezor.bip32_path.minimum", - })).toBe(true); - }) + expect( + interaction.hasMessagesFor({ + state: PENDING, + level: ERROR, + code: "trezor.bip32_path.minimum", + }) + ).toBe(true); + }); it("uses TrezorConnect.getPublicKey", () => { const interaction = interactionBuilder(); @@ -196,11 +197,9 @@ describe('trezor', () => { expect(params.coin).toEqual(trezorCoin(MAINNET)); expect(params.crossChain).toBe(true); }); - }); describe("TrezorExportPublicKey", () => { - const bip32Path = "m/45'/0'/0'/0'"; function interactionBuilder() { @@ -214,7 +213,9 @@ describe('trezor', () => { itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); it("parses out the public key from the response payload", () => { - expect(interactionBuilder().parsePayload({publicKey: "foobar"})).toEqual("foobar"); + expect( + interactionBuilder().parsePayload({ publicKey: "foobar" }) + ).toEqual("foobar"); }); it("uses TrezorConnect.getPublicKey", () => { @@ -225,11 +226,9 @@ describe('trezor', () => { expect(params.coin).toEqual(trezorCoin(MAINNET)); expect(params.crossChain).toBe(true); }); - }); describe("TrezorExportExtendedPublicKey", () => { - const bip32Path = "m/45'/0'/0'/0'"; function interactionBuilder() { @@ -243,7 +242,9 @@ describe('trezor', () => { itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); it("parses out the extended public key from the response payload", () => { - expect(interactionBuilder().parsePayload({xpub: "foobar"})).toEqual("foobar"); + expect(interactionBuilder().parsePayload({ xpub: "foobar" })).toEqual( + "foobar" + ); }); it("uses TrezorConnect.getPublicKey", () => { @@ -254,16 +255,14 @@ describe('trezor', () => { expect(params.coin).toEqual(trezorCoin(MAINNET)); expect(params.crossChain).toBe(true); }); - }); describe("TrezorSignMultisigTransaction", () => { - TEST_FIXTURES.transactions.forEach((fixture) => { - describe(`signing for a transaction which ${fixture.description}`, () => { - - function interactionBuilder() { return new TrezorSignMultisigTransaction(fixture); } + function interactionBuilder() { + return new TrezorSignMultisigTransaction(fixture); + } itHasStandardMessages(interactionBuilder); itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); @@ -274,12 +273,18 @@ describe('trezor', () => { // second byte is length of signature in bytes (0x03) // The string length is however long the signature is minus these two starting bytes // plain signature without SIGHASH (foobar is 3 bytes, string length = 6, which is 3 bytes) - expect(interactionBuilder().parsePayload({signatures: ["3003foobar"]})).toEqual(["3003foobar01"]); + expect( + interactionBuilder().parsePayload({ signatures: ["3003foobar"] }) + ).toEqual(["3003foobar01"]); // signature actually ends in 0x01 (foob01 is 3 bytes, string length = 6, which is 3 bytes) - expect(interactionBuilder().parsePayload({signatures: ["3003foob01"]})).toEqual(["3003foob0101"]); + expect( + interactionBuilder().parsePayload({ signatures: ["3003foob01"] }) + ).toEqual(["3003foob0101"]); // signature with sighash already included (foobar is 3 bytes, string length = 8, which is 4 bytes) ... // we expect this to chop off the 01 and add it back - expect(interactionBuilder().parsePayload({signatures: ["3003foobar01"]})).toEqual(["3003foobar01"]); + expect( + interactionBuilder().parsePayload({ signatures: ["3003foobar01"] }) + ).toEqual(["3003foobar01"]); }); it("uses TrezorConnect.signTransaction", () => { @@ -291,9 +296,7 @@ describe('trezor', () => { expect(params.outputs.length).toEqual(fixture.outputs.length); // FIXME check inputs & output details }); - }); - }); function psbtInteractionBuilder(tx, keyDetails, returnSignatureArray) { @@ -321,7 +324,9 @@ describe('trezor', () => { expect(params.inputs.length).toEqual(tx.inputs.length); expect(params.outputs.length).toEqual(tx.outputs.length); - expect(interaction.parsePayload({signatures: tx.signature})).toContain(PSBT_MAGIC_B64) + expect(interaction.parsePayload({ signatures: tx.signature })).toContain( + PSBT_MAGIC_B64 + ); }); it("uses TrezorConnect.signTransaction via PSBT for mainnet P2SH tx", () => { @@ -336,18 +341,21 @@ describe('trezor', () => { expect(params.coin).toEqual(trezorCoin(tx.network)); expect(params.inputs.length).toEqual(tx.inputs.length); expect(params.outputs.length).toEqual(tx.outputs.length); - expect(interaction.parsePayload({signatures: ["3003foobar01"]})).toEqual(["3003foobar01"]); + expect( + interaction.parsePayload({ signatures: ["3003foobar01"] }) + ).toEqual(["3003foobar01"]); }); - }); describe("TrezorConfirmMultisigAddress", () => { let TMP_FIXTURES = JSON.parse(JSON.stringify(TEST_FIXTURES)); TMP_FIXTURES.multisigs.forEach((fixture) => { - Reflect.deleteProperty(fixture, 'publicKey'); + Reflect.deleteProperty(fixture, "publicKey"); describe(`displaying a ${fixture.description}`, () => { - function interactionBuilder() { return new TrezorConfirmMultisigAddress(fixture); } + function interactionBuilder() { + return new TrezorConfirmMultisigAddress(fixture); + } itHasStandardMessages(interactionBuilder); itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); @@ -363,16 +371,14 @@ describe('trezor', () => { expect(params.crossChain).toBe(true); // FIXME check multisig details }); - }); - }); TEST_FIXTURES.multisigs.forEach((fixture) => { - describe(`displaying a ${fixture.description}`, () => { - - function interactionBuilder() { return new TrezorConfirmMultisigAddress(fixture); } + function interactionBuilder() { + return new TrezorConfirmMultisigAddress(fixture); + } itHasStandardMessages(interactionBuilder); itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); @@ -384,7 +390,7 @@ describe('trezor', () => { expect(params.bundle[0].path).toEqual(fixture.bip32Path); expect(params.bundle[0].showOnTrezor).toBe(false); expect(params.bundle[0].coin).toEqual(trezorCoin(fixture.network)); - expect(params.bundle[0].crossChain).toBe(true); + expect(params.bundle[0].crossChain).toBe(true); expect(params.bundle[1].path).toEqual(fixture.bip32Path); expect(params.bundle[1].address).toEqual(fixture.address); expect(params.bundle[1].showOnTrezor).toBe(true); @@ -399,9 +405,8 @@ describe('trezor', () => { TEST_FIXTURES.multisigs.forEach((fixture) => { it("passes through payload if payload address matches addresses for the public key", () => { function createAddress(publicKey, network) { - const keyPair = ECPair.fromPublicKey( - Buffer.from(publicKey, 'hex')); - const {address} = payments.p2pkh({ + const keyPair = ECPair.fromPublicKey(Buffer.from(publicKey, "hex")); + const { address } = payments.p2pkh({ pubkey: keyPair.publicKey, network: networkData(network), }); @@ -410,13 +415,16 @@ describe('trezor', () => { const interaction = new TrezorConfirmMultisigAddress(fixture); const address = createAddress(fixture.publicKey, fixture.network); - const payload = [{address}, {address}] + const payload = [{ address }, { address }]; const result = interaction.parsePayload(payload); expect(result).toEqual(payload); }); it("errors if payload has no matching address", () => { const interaction = new TrezorConfirmMultisigAddress(fixture); - const payload = [{address: "not matching"}, {address: "not matching"}] + const payload = [ + { address: "not matching" }, + { address: "not matching" }, + ]; expect(() => { interaction.parsePayload(payload); }).toThrow("Wrong public key specified"); @@ -425,10 +433,10 @@ describe('trezor', () => { it("passes through payload if there's no public key", () => { const fixture = TEST_FIXTURES.multisigs[0]; - const fixtureCopy = {...fixture} - Reflect.deleteProperty(fixtureCopy, 'publicKey'); + const fixtureCopy = { ...fixture }; + Reflect.deleteProperty(fixtureCopy, "publicKey"); const interaction = new TrezorConfirmMultisigAddress(fixtureCopy); - const payload = [] + const payload = []; const result = interaction.parsePayload(payload); expect(result).toEqual(payload); }); @@ -449,17 +457,19 @@ describe('trezor', () => { itHasStandardMessages(interactionBuilder); itThrowsAnErrorOnAnUnsuccessfulRequest(interactionBuilder); - it('constructor adds error message on invalid bip32path', () => { + it("constructor adds error message on invalid bip32path", () => { const interaction = new TrezorSignMessage({ - bip32Path: 'm/foo', + bip32Path: "m/foo", network: MAINNET, }); - expect(interaction.hasMessagesFor({ - state: PENDING, - level: ERROR, - code: "trezor.bip32_path.path_error", - })).toBe(true); - }) + expect( + interaction.hasMessagesFor({ + state: PENDING, + level: ERROR, + code: "trezor.bip32_path.path_error", + }) + ).toBe(true); + }); it("uses TrezorConnect.signMessage", () => { const interaction = interactionBuilder(); @@ -470,7 +480,6 @@ describe('trezor', () => { }); }); - // describe("Test interactions.", () => { // describe("Test public key export interactions.", () => {