From 632d45593fe742186609bffbc1b35c3ed2e59580 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Fri, 2 Jun 2023 17:28:23 +0900 Subject: [PATCH] multi: Add ledger backend functions. --- app/actions/LedgerActions.js | 196 +++++++++++++++++++++++++++++++ app/helpers/ledger.js | 183 +++++++++++++++++++++++++++++ app/main.development.js | 13 ++ package.json | 2 + test/mocks/walletMock.js | 94 +++++++++++++++ test/unit/helpers/ledger.spec.js | 28 +++++ yarn.lock | 152 ++++++++++++++++++++++-- 7 files changed, 657 insertions(+), 11 deletions(-) create mode 100644 app/actions/LedgerActions.js create mode 100644 app/helpers/ledger.js create mode 100644 test/unit/helpers/ledger.spec.js diff --git a/app/actions/LedgerActions.js b/app/actions/LedgerActions.js new file mode 100644 index 0000000000..0b356681b5 --- /dev/null +++ b/app/actions/LedgerActions.js @@ -0,0 +1,196 @@ +import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; +import { createTransaction } from "@ledgerhq/hw-app-btc/lib/createTransaction"; +import Btc from "@ledgerhq/hw-app-btc"; +import * as ledgerHelpers from "../helpers/ledger"; +import { wallet } from "wallet-preload-shim"; +import { publishTransactionAttempt } from "./ControlActions"; +import { hexToBytes } from "helpers"; +import { + SIGNTX_ATTEMPT, + SIGNTX_FAILED, + SIGNTX_SUCCESS +} from "./ControlActions"; + +const coin = "decred"; + +import * as selectors from "selectors"; +import * as cfgConstants from "constants/config"; + +export const LDG_LEDGER_ENABLED = "LDG_LEDGER_ENABLED"; +export const LDG_WALLET_CLOSED = "LDG_WALLET_CLOSED"; + +// This is an error's message when an app is open but we are trying to get +// device info. +// const DEVICE_ON_DASHBOARD_EXPECTED = "DeviceOnDashboardExpected"; + +// enableLedger only sets a value in the config. Ledger connections are made +// per action then dropped. +export const enableLedger = () => (dispatch, getState) => { + const walletName = selectors.getWalletName(getState()); + + if (walletName) { + const config = wallet.getWalletCfg( + selectors.isTestNet(getState()), + walletName + ); + config.set(cfgConstants.LEDGER, true); + } + + dispatch({ type: LDG_LEDGER_ENABLED }); + + connect()(dispatch, getState); +}; + +export const LDG_CONNECT_ATTEMPT = "LDG_CONNECT_ATTEMPT"; +export const LDG_CONNECT_FAILED = "LDG_CONNECT_FAILED"; +export const LDG_CONNECT_SUCCESS = "LDG_CONNECT_SUCCESS"; + +// connect only checks that a connection does not error, so a device exists and +// is plugged in. +export const connect = () => async (dispatch /*, getState*/) => { + dispatch({ type: LDG_CONNECT_ATTEMPT }); + try { + await doWithTransport(async () => {}); + } catch (error) { + dispatch({ type: LDG_CONNECT_FAILED }); + throw error; + } + dispatch({ type: LDG_CONNECT_SUCCESS }); +}; + +export const LDG_LEDGER_DISABLED = "LDG_LEDGER_DISABLED"; + +// disableLedger disables ledger integration for the current wallet. Note +// that it does **not** disable in the config, so the wallet will restart as a +// ledger wallet next time it's opened. +export const disableLedger = () => (dispatch) => { + dispatch({ type: LDG_LEDGER_DISABLED }); +}; + +export const LDG_NOCONNECTEDDEVICE = "LDG_NOCONNECTEDDEVICE"; + +export const alertNoConnectedDevice = () => (dispatch) => { + dispatch({ type: LDG_NOCONNECTEDDEVICE }); +}; + +// checkLedgerIsDcrwallet verifies whether the wallet currently running on +// dcrwallet (presumably a watch only wallet created from a ledger provided +// xpub) is the same wallet as the one of the currently connected ledger. This +// function throws an error if they are not the same. +// This is useful for making sure, prior to performing some wallet related +// function such as transaction signing, that ledger will correctly perform the +// operation. +// Note that this might trigger pin/passphrase modals, depending on the current +// ledger configuration. +// The way the check is performed is by generating the first address from the +// ledger wallet and then validating this address agains dcrwallet, ensuring +// this is an owned address at the appropriate branch/index. +// This check is only valid for a single session (ie, a single execution of +// `deviceRun`) as the physical device might change between sessions. +const checkLedgerIsDcrwallet = () => async (dispatch, getState) => { + const { + grpc: { walletService } + } = getState(); + + const path = ledgerHelpers.addressPath(0, 0); + const payload = await getAddress(path); + const addr = payload.bitcoinAddress; + + const addrValidResp = await wallet.validateAddress(walletService, addr); + if (!addrValidResp.isValid) + throw "Ledger provided an invalid address " + addr; + + if (!addrValidResp.isMine) + throw "Ledger and dcrwallet not running from the same extended public key"; + + if (addrValidResp.index !== 0) throw "Wallet replied with wrong index."; +}; + +export const signTransactionAttemptLedger = + (rawUnsigTx) => async (dispatch, getState) => { + dispatch({ type: SIGNTX_ATTEMPT }); + const { + grpc: { walletService } + } = getState(); + const chainParams = selectors.chainParams(getState()); + + try { + const arg = await ledgerHelpers.signArg( + rawUnsigTx, + chainParams, + walletService, + dispatch + ); + + await dispatch(checkLedgerIsDcrwallet()); + const signedRaw = await createTx(arg); + if (signedRaw.message) { + throw signedRaw.message; + } + + dispatch({ type: SIGNTX_SUCCESS }); + dispatch(publishTransactionAttempt(hexToBytes(signedRaw))); + } catch (error) { + dispatch({ error, type: SIGNTX_FAILED }); + } + }; + +export const LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT = + "LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT"; +export const LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED = + "LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED"; +export const LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS = + "LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS"; + +export const getWalletCreationMasterPubKey = + () => async (dispatch /*, getState*/) => { + dispatch({ type: LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT }); + // TODO: Enable on mainnet. + const isTestnet = true; + try { + const payload = await getAddress("44'/42'/0'"); + const hdpk = ledgerHelpers.pubkeyToHDPubkey( + payload.publicKey, + payload.chainCode, + isTestnet + ); + + dispatch({ type: LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS }); + + return hdpk; + } catch (error) { + dispatch({ error, type: LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED }); + throw error; + } + }; + +function doWithTransport(fn) { + return TransportWebUSB.create() + .then((transport) => { + return fn(transport).then((r) => + transport + .close() + .catch((/*e*/) => {}) // throw? + .then(() => r) + ); + }) + .catch((e) => { + throw e; + }); +} + +function getAddress(path) { + const fn = async (transport) => { + const btc = new Btc({ transport, currency: coin }); + return await btc.getWalletPublicKey(path, { + verify: false + }); + }; + return doWithTransport(fn); +} + +function createTx(arg) { + return doWithTransport((transport) => { + return createTransaction(transport, arg); + }); +} diff --git a/app/helpers/ledger.js b/app/helpers/ledger.js new file mode 100644 index 0000000000..f03e381cbb --- /dev/null +++ b/app/helpers/ledger.js @@ -0,0 +1,183 @@ +import { wallet } from "wallet-preload-shim"; +import { hexToBytes, strHashToRaw } from "helpers"; +import * as secp256k1 from "secp256k1"; +import { default as blake } from "blake-hash"; +import * as bs58 from "bs58"; +import toBuffer from "typedarray-to-buffer"; +import { getTxFromInputs } from "../actions/TransactionActions"; + +export function addressPath(branch, index) { + const prefix = "44'/42'/0'/"; + const i = (index || 0).toString(); + const b = (branch || 0).toString(); + return prefix + b + "/" + i; +} + +export function pubkeyToHDPubkey(pubkey, chainCode, isTestnet) { + const pk = secp256k1.publicKeyConvert(hexToBytes(pubkey), true); // from uncompressed to compressed + const cc = hexToBytes(chainCode); + let hdPublicKeyID = hexToBytes("02fda926"); // dpub + if (isTestnet) { + hdPublicKeyID = hexToBytes("043587d1"); // tpub + } + const parentFP = hexToBytes("00000000"); // not true but we dont know the fingerprint + const childNum = hexToBytes("80000000"); // always first hardened child + const depth = 2; // account is depth 2 + const buff = new Uint8Array(78); // 4 network identifier + 1 depth + 4 parent fingerprint + 4 child number + 32 chain code + 33 compressed public key + let i = 0; + buff.set(hdPublicKeyID, i); + i += 4; + buff[i] = depth; + i += 1; + buff.set(parentFP, i); + i += 4; + buff.set(childNum, i); + i += 4; + buff.set(cc, i); + i += 32; + buff.set(pk, i); + const firstPass = blake("blake256").update(Buffer.from(buff)).digest(); + const secondPass = blake("blake256").update(firstPass).digest(); + const fullSerialize = Buffer.concat([ + Buffer.from(buff), + secondPass.slice(0, 4) + ]); + return bs58.encode(fullSerialize); +} + +function writeUint16LE(n) { + const buff = new Buffer(2); + buff.writeUInt16LE(n, 0); + return buff; +} + +function writeUint32LE(n) { + const buff = new Buffer(4); + buff.writeUInt32LE(n, 0); + return buff; +} + +function writeUint64LE(n) { + const buff = new Buffer(8); + const lower = 0xffffffff & n; + // bitshift right (>>>) does not seem to throw away the lower half, so + // dividing and throwing away the remainder. + const upper = Math.floor(n / 0xffffffff); + buff.writeUInt32LE(lower, 0); + buff.writeUInt32LE(upper, 4); + return buff; +} + +function inputToTx(tx) { + const inputs = []; + for (const inp of tx.inputs) { + const sequence = writeUint32LE(inp.sequence); + const tree = new Uint8Array(1); + tree[0] = inp.outputTree; + const prevout = new Uint8Array(36); + prevout.set(strHashToRaw(inp.prevTxId), 0); + prevout.set(writeUint32LE(inp.outputIndex), 32); + const input = { + prevout: toBuffer(prevout), + script: toBuffer(new Uint8Array(25)), + sequence: sequence, + tree: toBuffer(tree) + }; + inputs.push(input); + } + const outputs = []; + for (const out of tx.outputs) { + const output = { + amount: writeUint64LE(out.value), + script: out.script + }; + outputs.push(output); + } + return { + version: writeUint32LE(tx.version), // Pretty sure this is a uint16 but ledger does not want that. + inputs: inputs, + outputs: outputs, + locktime: writeUint32LE(tx.lockTime), + nExpiryHeight: writeUint32LE(tx.expiry) + }; +} + +function createPrefix(tx) { + const numOuts = tx.outputs.length; + if (numOuts > 2) { + throw "more than two outputs is not expected"; + } + let buffLen = 1; + for (const out of tx.outputs) { + buffLen += 11 + out.script.length; + } + const buff = new Uint8Array(buffLen); // 1 varInt + ( 8 value + 2 tx version + 1 varInt + (23/25?) variable script length) * number of outputs + let i = 0; + buff[i] = numOuts; + i += 1; + for (const out of tx.outputs) { + buff.set(writeUint64LE(out.value), i); + i += 8; + buff.set(writeUint16LE(out.version), i); + i += 2; + // TODO: Clean this up for production? Should use smarter logic to get varInts? + buff[i] = out.script.length; // varInt for 23/25 bytes + i += 1; + buff.set(out.script, i); + i += out.script.length; + } + return toBuffer(buff); +} + +export async function signArg(txHex, chainParams, walletService, dispatch) { + const tx = await wallet.decodeTransactionLocal(txHex, chainParams); + const inputTxs = await dispatch(getTxFromInputs(tx)); + const inputs = []; + const paths = []; + for (const inp of tx.inputs) { + let verboseInp; + for (const it of inputTxs) { + if (it.hash === inp.prevTxId) { + verboseInp = it; + break; + } + } + if (!verboseInp) { + throw "cound not find input"; + } + const prevOut = inputToTx(verboseInp); + const idx = inp.outputIndex; + inputs.push([prevOut, idx]); + const addr = verboseInp.outputs[idx].decodedScript.address; + const val = await wallet.validateAddress(walletService, addr); + const acct = val.accountNumber.toString(); + const branch = val.isInternal ? "1" : "0"; + const index = val.index.toString(); + paths.push("44'/42'/" + acct + "'/" + branch + "/" + index); + } + let changePath = null; + for (const out of tx.outputs) { + const addr = out.decodedScript.address; + const val = await wallet.validateAddress(walletService, addr); + if (!val.isInternal) { + continue; + } // assume the internal address is change + const acct = val.accountNumber.toString(); + const index = val.index.toString(); + changePath = "44'/42'/" + acct + "'/1/" + index; + break; + } + + return { + inputs: inputs, + associatedKeysets: paths, + changePath: changePath, + outputScriptHex: createPrefix(tx), + lockTime: tx.lockTime, + sigHashType: 1, // SIGHASH_ALL + segwit: false, + expiryHeight: writeUint32LE(tx.expiry), + useTrustedInputForSegwit: false, + additionals: ["decred"] + }; +} diff --git a/app/main.development.js b/app/main.development.js index a7c1344abb..e7d6b17726 100644 --- a/app/main.development.js +++ b/app/main.development.js @@ -873,6 +873,19 @@ app.on("ready", async () => { height: 1000, icon: __dirname + "/dcrdex.png" }); + + mainWindow.webContents.session.setDevicePermissionHandler((details) => { + // Allow Ledger devices which share a unique vendor ID. + if ( + details.deviceType === "usb" && + details.origin === "http://localhost:3000" && + details.device && + details.device.vendorId === 0x2c97 + ) { + return true; + } + return false; + }); }); app.on("before-quit", async (event) => { diff --git a/package.json b/package.json index 1332d1e9b2..7a92245444 100644 --- a/package.json +++ b/package.json @@ -273,6 +273,8 @@ "@formatjs/intl-utils": "^1.6.0", "@grpc/grpc-js": "1.7.3", "@hot-loader/react-dom": "16.14.0", + "@ledgerhq/hw-app-btc": "^10.0.5", + "@ledgerhq/hw-transport-webusb": "^6.27.16", "@peculiar/webcrypto": "1.4.3", "@xstate/react": "^0.8.1", "blake-hash": "^2.0.0", diff --git a/test/mocks/walletMock.js b/test/mocks/walletMock.js index 54f59301c3..21c897b9fb 100644 --- a/test/mocks/walletMock.js +++ b/test/mocks/walletMock.js @@ -28,3 +28,97 @@ export const getAvailableWallets = jest.fn(() => []); export const getPreviousWallet = jest.fn(() => null); export const decodeRawTransaction = jest.fn((...args) => drt(...args)); + +const vaMap = { + TsVRPhzyuvCuxPipBx2YeLarrTPPEAsv4Bh: { + isValid: true, + isMine: true, + accountNumber: 0, + pubKeyAddr: "TkQ4CFL6gY6iF87MnP1a2hrFZeZ9wmR7uU1aaiVPBiVEE8iaMCXyK", + pubKey: "A20UfEkHp8ZG4WZc24AQ4c0gYrkkwGOKeeW9LDG75yij", + isScript: false, + pkScriptAddrsList: [], + scriptType: 2, + payToAddrScript: "dqkUMEbI+i8hd+yvj70Ll1XrcOkp0peIrA==", + sigsRequired: 0, + isInternal: true, + index: 71 + }, + TsiHcAi5c2CpMSmQm8BasAoYgMr91y6Pq3h: { + isValid: true, + isMine: true, + accountNumber: 0, + pubKeyAddr: "TkQ3bghS4na8dL6kuXwTVvE3Jhe7LH2NbxPbVMkFYZtz412vsk8Qu", + pubKey: "Ax6Z8n4/KE0W0bKpdqksPg0KIDxv8sXXRFStW8sgFz/V", + isScript: false, + pkScriptAddrsList: [], + scriptType: 2, + payToAddrScript: "dqkUvWd+epUkLC1UIn45KM7Yicw6rqeIrA==", + sigsRequired: 0, + isInternal: true, + index: 72 + } +}; + +const dtlReturn = { + version: 1, + serType: 0, + numInputs: 1, + inputs: [ + { + prevTxId: + "d29e701778f726ca5f1b50dddb9529f61911732586fa032eee9c2418f617ce89", + outputIndex: 0, + outputTree: 0, + sequence: 4294967295, + index: 0, + valueIn: 106606220, + blockHeight: 0, + blockIndex: 4294967295, + sigScript: {} + } + ], + numOutputs: 2, + outputs: [ + { + value: 105373690, + version: 0, + script: Buffer.from([ + 118, 169, 20, 189, 103, 126, 122, 149, 36, 44, 45, 84, 34, 126, 57, 40, + 206, 216, 137, 204, 58, 174, 167, 136, 172 + ]), + index: 0, + decodedScript: { + scriptClass: 2, + address: "TsiHcAi5c2CpMSmQm8BasAoYgMr91y6Pq3h", + requiredSig: 1, + asm: "OP_DUP OP_HASH160 OP_DATA_20 bd677e7a95242c2d54227e3928ced889cc3aaea7 OP_EQUALVERIFY OP_CHECKSIG" + } + }, + { + value: 1230000, + version: 0, + script: Buffer.from([ + 118, 169, 20, 210, 81, 146, 28, 152, 87, 121, 24, 21, 233, 119, 80, 175, + 206, 164, 143, 189, 48, 86, 138, 136, 172 + ]), + index: 1, + decodedScript: { + scriptClass: 2, + address: "TskCC6UkcKfBRsrJVPhSjwGjn7ptD34HA4T", + requiredSig: 1, + asm: "OP_DUP OP_HASH160 OP_DATA_20 d251921c9857791815e97750afcea48fbd30568a OP_EQUALVERIFY OP_CHECKSIG" + } + } + ], + lockTime: 0, + expiry: 0 +}; + +export const decodeTransactionLocal = jest.fn(() => { + return dtlReturn; +}); +export const validateAddress = jest.fn((...args) => { + return vaMap[args[1]]; +}); +export const getTxFromInputs = jest.fn(() => null); diff --git a/test/unit/helpers/ledger.spec.js b/test/unit/helpers/ledger.spec.js new file mode 100644 index 0000000000..3ed99481a8 --- /dev/null +++ b/test/unit/helpers/ledger.spec.js @@ -0,0 +1,28 @@ +import { addressPath, pubkeyToHDPubkey, signArg } from "helpers/ledger"; + +test("test ledger address path", () => { + expect(addressPath(1, 2)).toStrictEqual("44'/42'/0'/1/2"); +}); + +test("test ledger pubkey to hd key", () => { + // testnet + expect(pubkeyToHDPubkey("043e3e8802449f60621bebed4fcb296c781b63df429e45b1201fda6d4ec6a172445ffe4f6a5032de88cd1ba4397c9a397fe9522d76e12306c8f38b8fa61d4bdc33", + "7271b9c5a3dbf281efdab76250d978f178782e33b354b131c0b817459c98bdc3", true)).toStrictEqual("tpubVmYdgyojvygmmGAKaWc8qCzFGafGFVNhGr44tra11QGak5GwMsVibCtkdvxYfg27XgHGvtN6j9kt9p8cewEChPM5HBqaeQGMS1E4WgSY271"); + // mainet + expect(pubkeyToHDPubkey("043e3e8802449f60621bebed4fcb296c781b63df429e45b1201fda6d4ec6a172445ffe4f6a5032de88cd1ba4397c9a397fe9522d76e12306c8f38b8fa61d4bdc33", + "7271b9c5a3dbf281efdab76250d978f178782e33b354b131c0b817459c98bdc3", false)).toStrictEqual("dpubZCmMrtFLzkfwgoEsrkwMkAH5i49zt3yGkEuwe4nQBEsnuFz8erXctxL4ehVtDBnDqYXKJEKpCygt7qt298XHpEHko3vDtuvE2z8NzLkPZTo"); +}); + +function dispatchFn (inputs) { + return function () { + return inputs; + }; +} + +test("test ledger constructing arg to sign", async () => { + const txHex = "010000000189ce17f618249cee2e03fa8625731119f62995dbdd501b5fca26f77817709ed20000000000ffffffff02fadf47060000000000001976a914bd677e7a95242c2d54227e3928ced889cc3aaea788acb0c412000000000000001976a914d251921c9857791815e97750afcea48fbd30568a88ac0000000000000000018cae5a060000000000000000ffffffff00"; + const mockDispatch = dispatchFn([{ "version": 1, "serType": 0, "numInputs": 1, "inputs": [{ "prevTxId": "9874aacfe5b9179092f260882807e7088e7fca96fbce908999f31f42c37158be", "outputIndex": 0, "outputTree": 0, "sequence": 4294967295, "index": 0, "valueIn": 0, "blockHeight": 0, "blockIndex": 4294967295, "sigScript": { "0": 71, "1": 48, "2": 68, "3": 2, "4": 32, "5": 91, "6": 228, "7": 127, "8": 22, "9": 156, "10": 231, "11": 152, "12": 180, "13": 69, "14": 163, "15": 217, "16": 181, "17": 246, "18": 76, "19": 150, "20": 237, "21": 250, "22": 47, "23": 87, "24": 202, "25": 47, "26": 129, "27": 182, "28": 234, "29": 133, "30": 42, "31": 191, "32": 188, "33": 219, "34": 16, "35": 99, "36": 68, "37": 2, "38": 32, "39": 104, "40": 94, "41": 54, "42": 96, "43": 225, "44": 218, "45": 133, "46": 125, "47": 122, "48": 238, "49": 201, "50": 207, "51": 102, "52": 34, "53": 30, "54": 199, "55": 128, "56": 204, "57": 74, "58": 161, "59": 142, "60": 18, "61": 14, "62": 185, "63": 194, "64": 34, "65": 150, "66": 21, "67": 130, "68": 145, "69": 244, "70": 114, "71": 1, "72": 33, "73": 3, "74": 51, "75": 241, "76": 244, "77": 132, "78": 192, "79": 73, "80": 61, "81": 162, "82": 82, "83": 113, "84": 188, "85": 186, "86": 229, "87": 98, "88": 168, "89": 15, "90": 128, "91": 182, "92": 75, "93": 174, "94": 184, "95": 118, "96": 235, "97": 168, "98": 34, "99": 43, "100": 42, "101": 98, "102": 50, "103": 181, "104": 90, "105": 163 } }], "numOutputs": 2, "outputs": [{ "value": 106606220, "version": 0, "script": Buffer.from([118, 169, 20, 48, 70, 200, 250, 47, 33, 119, 236, 175, 143, 189, 11, 151, 85, 235, 112, 233, 41, 210, 151, 136, 172]), "index": 0, "decodedScript": { "scriptClass": 2, "address": "TsVRPhzyuvCuxPipBx2YeLarrTPPEAsv4Bh", "requiredSig": 1, "asm": "OP_DUP OP_HASH160 OP_DATA_20 3046c8fa2f2177ecaf8fbd0b9755eb70e929d297 OP_EQUALVERIFY OP_CHECKSIG" } }, { "value": 1230000, "version": 0, "script": Buffer.from([118, 169, 20, 210, 81, 146, 28, 152, 87, 121, 24, 21, 233, 119, 80, 175, 206, 164, 143, 189, 48, 86, 138, 136, 172]), "index": 1, "decodedScript": { "scriptClass": 2, "address": "TskCC6UkcKfBRsrJVPhSjwGjn7ptD34HA4T", "requiredSig": 1, "asm": "OP_DUP OP_HASH160 OP_DATA_20 d251921c9857791815e97750afcea48fbd30568a OP_EQUALVERIFY OP_CHECKSIG" } }], "lockTime": 0, "expiry": 0, "hash": "d29e701778f726ca5f1b50dddb9529f61911732586fa032eee9c2418f617ce89" }]); + const want = { "inputs": [[{ "version": { "type": "Buffer", "data": [1, 0, 0, 0] }, "inputs": [{ "prevout": { "type": "Buffer", "data": [190, 88, 113, 195, 66, 31, 243, 153, 137, 144, 206, 251, 150, 202, 127, 142, 8, 231, 7, 40, 136, 96, 242, 146, 144, 23, 185, 229, 207, 170, 116, 152, 0, 0, 0, 0] }, "script": { "type": "Buffer", "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, "sequence": { "type": "Buffer", "data": [255, 255, 255, 255] }, "tree": { "type": "Buffer", "data": [0] } }], "outputs": [{ "amount": { "type": "Buffer", "data": [140, 174, 90, 6, 0, 0, 0, 0] }, "script": { "type": "Buffer", "data": [118, 169, 20, 48, 70, 200, 250, 47, 33, 119, 236, 175, 143, 189, 11, 151, 85, 235, 112, 233, 41, 210, 151, 136, 172] } }, { "amount": { "type": "Buffer", "data": [176, 196, 18, 0, 0, 0, 0, 0] }, "script": { "type": "Buffer", "data": [118, 169, 20, 210, 81, 146, 28, 152, 87, 121, 24, 21, 233, 119, 80, 175, 206, 164, 143, 189, 48, 86, 138, 136, 172] } }], "locktime": { "type": "Buffer", "data": [0, 0, 0, 0] }, "nExpiryHeight": { "type": "Buffer", "data": [0, 0, 0, 0] } }, 0]], "associatedKeysets": ["44'/42'/0'/1/71"], "changePath": "44'/42'/0'/1/72", "outputScriptHex": { "type": "Buffer", "data": [2, 250, 223, 71, 6, 0, 0, 0, 0, 0, 0, 25, 118, 169, 20, 189, 103, 126, 122, 149, 36, 44, 45, 84, 34, 126, 57, 40, 206, 216, 137, 204, 58, 174, 167, 136, 172, 176, 196, 18, 0, 0, 0, 0, 0, 0, 0, 25, 118, 169, 20, 210, 81, 146, 28, 152, 87, 121, 24, 21, 233, 119, 80, 175, 206, 164, 143, 189, 48, 86, 138, 136, 172] }, "lockTime": 0, "sigHashType": 1, "segwit": false, "expiryHeight": { "type": "Buffer", "data": [0, 0, 0, 0] }, "useTrustedInputForSegwit": false, "additionals": ["decred"] }; + const data = await signArg(txHex, null, null, mockDispatch); + expect(JSON.stringify(data)).toStrictEqual(JSON.stringify(want)); +}); diff --git a/yarn.lock b/yarn.lock index 8b35365cdb..18f30b2d85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1795,6 +1795,63 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@ledgerhq/devices@^8.0.4": + version "8.0.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.0.4.tgz#ebc7779adbbec2d046424603a481623eb3fbe306" + integrity sha512-dxOiWZmtEv1tgw70+rW8gviCRZUeGDUnxY6HUPiRqTAc0Ts2AXxiJChgAsPvIywWTGW+S67Nxq1oTZdpRbdt+A== + dependencies: + "@ledgerhq/errors" "^6.12.7" + "@ledgerhq/logs" "^6.10.1" + rxjs "6" + semver "^7.3.5" + +"@ledgerhq/errors@^6.12.7": + version "6.12.7" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.12.7.tgz#c7b630488d5713bc7b1e1682d6ab5d08918c69f1" + integrity sha512-1BpjzFErPK7qPFx0oItcX0mNLJMplVAm2Dpl5urZlubewnTyyw5sahIBjU+8LLCWJ2eGEh/0wyvh0jMtR0n2Mg== + +"@ledgerhq/hw-app-btc@^10.0.5": + version "10.0.5" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-10.0.5.tgz#2125480db24ae7c508d3922aeb990dca884ca5a6" + integrity sha512-X1QfEvkA/XP7x6/vg5+6iWTIaEc79gZ5c+qU2X/HzUAOJQK8uJtNX0/68nEAObUHNhbN8BrOrUo40ARbPDqNgA== + dependencies: + "@ledgerhq/hw-transport" "^6.28.5" + "@ledgerhq/logs" "^6.10.1" + bip32-path "^0.4.2" + bitcoinjs-lib "^5.2.0" + bs58 "^4.0.1" + bs58check "^2.1.2" + invariant "^2.2.4" + ripemd160 "2" + semver "^7.3.5" + sha.js "2" + tiny-secp256k1 "1.1.6" + varuint-bitcoin "1.1.2" + +"@ledgerhq/hw-transport-webusb@^6.27.16": + version "6.27.16" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.27.16.tgz#b8e20e772f78c312fc7f2ce3a469c99ecf59dc67" + integrity sha512-A3S2p5Rh9Ot402pWNZw8v5EpO3wOHP8ch/Dcz0AjInmwNouQ9nIYd1+eLSL7QiyG9X7+tuHxFF1IjrEgvAzQuQ== + dependencies: + "@ledgerhq/devices" "^8.0.4" + "@ledgerhq/errors" "^6.12.7" + "@ledgerhq/hw-transport" "^6.28.5" + "@ledgerhq/logs" "^6.10.1" + +"@ledgerhq/hw-transport@^6.28.5": + version "6.28.5" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.28.5.tgz#675193be2f695a596068145351da598316c25831" + integrity sha512-xmw5RhYbqExBBqTvOnOjN/RYNIGMBxFJ+zcYNfkfw/E+uEY3L7xq8Z7sC/n7URTT6xtEctElqduBJnBQE4OQtw== + dependencies: + "@ledgerhq/devices" "^8.0.4" + "@ledgerhq/errors" "^6.12.7" + events "^3.3.0" + +"@ledgerhq/logs@^6.10.1": + version "6.10.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.10.1.tgz#5bd16082261d7364eabb511c788f00937dac588d" + integrity sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w== + "@malept/cross-spawn-promise@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" @@ -2547,6 +2604,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb" integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ== +"@types/node@10.12.18": + version "10.12.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" + integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== + "@types/node@^16.11.26": version "16.18.36" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.36.tgz#0db5d7efc4760d36d0d1d22c85d1a53accd5dc27" @@ -3633,6 +3695,11 @@ bchaddrjs@^0.5.2: cashaddrjs "0.4.4" stream-browserify "^3.0.0" +bech32@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + bech32@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" @@ -3670,18 +3737,62 @@ bindings@~1.2.1: resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" integrity sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g== -bip66@^1.1.5: +bip174@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.0.tgz#cd3402581feaa5116f0f00a0eaee87a5843a2d30" + integrity sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA== + +bip32-path@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/bip32-path/-/bip32-path-0.4.2.tgz#5db0416ad6822712f077836e2557b8697c0c7c99" + integrity sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ== + +bip32@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-2.0.6.tgz#6a81d9f98c4cd57d05150c60d8f9e75121635134" + integrity sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA== + dependencies: + "@types/node" "10.12.18" + bs58check "^2.1.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + tiny-secp256k1 "^1.1.3" + typeforce "^1.11.5" + wif "^2.0.6" + +bip66@^1.1.0, bip66@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" integrity sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw== dependencies: safe-buffer "^5.0.1" -bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.1: +bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.0, bitcoin-ops@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow== +bitcoinjs-lib@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-5.2.0.tgz#caf8b5efb04274ded1b67e0706960b93afb9d332" + integrity sha512-5DcLxGUDejgNBYcieMIUfjORtUeNWl828VWLHJGVKZCb4zIS1oOySTUr0LGmcqJBQgTBz3bGbRQla4FgrdQEIQ== + dependencies: + bech32 "^1.1.2" + bip174 "^2.0.1" + bip32 "^2.0.4" + bip66 "^1.1.0" + bitcoin-ops "^1.4.0" + bs58check "^2.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.3" + merkle-lib "^2.0.10" + pushdata-bitcoin "^1.0.1" + randombytes "^2.0.1" + tiny-secp256k1 "^1.1.1" + typeforce "^1.11.3" + varuint-bitcoin "^1.0.4" + wif "^2.0.1" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -3907,7 +4018,7 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" -bs58check@2.1.2, bs58check@<3.0.0, bs58check@^2.1.1: +bs58check@2.1.2, bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== @@ -4746,7 +4857,7 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hmac@^1.1.0, create-hmac@^1.1.3, create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -7491,6 +7602,13 @@ intl-messageformat@^7.8.4: intl-format-cache "^4.2.21" intl-messageformat-parser "^3.6.4" +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + ip@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" @@ -9175,6 +9293,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merkle-lib@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/merkle-lib/-/merkle-lib-2.0.10.tgz#82b8dbae75e27a7785388b73f9d7725d0f6f3326" + integrity sha512-XrNQvUbn1DL5hKNe46Ccs+Tu3/PYOlrcZILuGUhb95oKBPjc/nmIC8D462PQkipVDGKRvwhn+QFg2cCdIvmDJA== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -11859,7 +11982,7 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: +ripemd160@2, ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== @@ -11960,6 +12083,13 @@ runtypes@^6.5.1: resolved "https://registry.yarnpkg.com/runtypes/-/runtypes-6.6.0.tgz#48e353d8b0f641ab5ec5d80fa96dd7bd41ea3281" integrity sha512-ddM7sgB3fyboDlBzEYFQ04L674sKjbs4GyW2W32N/5Ae47NRd/GyMASPC2PFw8drPHYGEcZ0mZ26r5RcB8msfQ== +rxjs@6: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -12191,7 +12321,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.8: +sha.js@2, sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -12983,7 +13113,7 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== -tiny-secp256k1@^1.1.6: +tiny-secp256k1@1.1.6, tiny-secp256k1@^1.1.1, tiny-secp256k1@^1.1.3, tiny-secp256k1@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz#7e224d2bee8ab8283f284e40e6b4acb74ffe047c" integrity sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA== @@ -13133,7 +13263,7 @@ tsconfig-paths@^3.10.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1: +tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -13235,7 +13365,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typeforce@^1.18.0: +typeforce@^1.11.3, typeforce@^1.11.5, typeforce@^1.18.0: version "1.18.0" resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== @@ -13557,7 +13687,7 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== -varuint-bitcoin@^1.1.2: +varuint-bitcoin@1.1.2, varuint-bitcoin@^1.0.4, varuint-bitcoin@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz#e76c138249d06138b480d4c5b40ef53693e24e92" integrity sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw== @@ -13898,7 +14028,7 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -wif@^2.0.6: +wif@^2.0.1, wif@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" integrity sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==