From 4b6390bad74e9d61e08574ab8648fd5be2e012af Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 14 Oct 2026 03:09:03 +0000 Subject: [PATCH] fix: sign and export with each address's own derivation index (closes #510) Sending, site transactions, site signatures and private key export derived the key at the address's position in the wallet's list. After an import whose scan passed over an unused index, or after removing an address that was not the last, that position named another address's key. Each HD and xprv address record now stores the index it was derived at (at wallet creation, by the import scan, by "+"), and getSignerForAddress() derives at that index and refuses any key whose address differs from the record's. The export screen reports that refusal instead of calling the password wrong. Model: opus-5-5 --- README.md | 12 +- src/popup/views/addWallet.js | 16 +- src/popup/views/approval.js | 14 +- src/popup/views/confirmTx.js | 2 +- src/popup/views/exportPrivkey.js | 13 +- src/popup/views/home.js | 1 + src/shared/wallet.js | 26 ++- tests/addressKey.test.js | 386 +++++++++++++++++++++++++++++++ tests/approvalNonce.test.js | 4 +- tests/approvalVerify.test.js | 12 +- tests/wallet.test.js | 33 ++- tests/walletDefects.test.js | 6 +- 12 files changed, 496 insertions(+), 29 deletions(-) create mode 100644 tests/addressKey.test.js diff --git a/README.md b/README.md index 751d174..ea578e9 100644 --- a/README.md +++ b/README.md @@ -1188,7 +1188,14 @@ The core hierarchy is **Wallets → Addresses**: of its stored `xpub` by `src/shared/walletDefects.js`, explained in the wallet list, and blocked from signing, sending and private-key export. It is never deleted or rewritten. -- An **address** holds ETH and ERC-20 tokens. +- An **address** holds ETH and ERC-20 tokens. In an HD or xprv wallet each + address record also stores `index`, the derivation index it was made at: 0 for + the first address, the index the import scan found it at, or `nextIndex` when + "+" added it. An address's `index` and its position in the wallet's list + differ once the scan has passed over an unused index or an address has been + removed. Sending, site transactions, site signatures and private-key export + derive the key at `index`, and refuse it unless it is that address's own key + (`getSignerForAddress()` in `src/shared/wallet.js`). - The user can have multiple wallets, each with multiple addresses (HD) or a single address (key). @@ -1514,6 +1521,9 @@ view would leave a wallet one click from deletion. change) - "Reveal" (wrong password) → full-sentence error on the error line, nothing revealed (no screen change) + - "Reveal" (correct password, but the key derived is not this address's own, + see Data Model) → full-sentence error saying so, nothing revealed (no + screen change) - "Back" → previous screen (AddressDetail) - Settings gear → **Settings**, whose "Back" goes to AddressDetail: leaving drops the address the screen was showing, so it also takes the screen off diff --git a/src/popup/views/addWallet.js b/src/popup/views/addWallet.js index b6e0c3e..660061a 100644 --- a/src/popup/views/addWallet.js +++ b/src/popup/views/addWallet.js @@ -184,7 +184,12 @@ async function importMnemonic(ctx) { encryptedSecret: encrypted, nextIndex: 1, addresses: [ - { address: firstAddress, balance: "0.0000", tokenBalances: [] }, + { + address: firstAddress, + index: 0, + balance: "0.0000", + tokenBalances: [], + }, ], }; state.wallets.push(wallet); @@ -205,6 +210,7 @@ async function importMnemonic(ctx) { if (scan.addresses.length > 1) { wallet.addresses = scan.addresses.map((a) => ({ address: a.address, + index: a.index, balance: "0.0000", tokenBalances: [], })); @@ -299,7 +305,12 @@ async function importXprvKey(ctx) { encryptedSecret: encrypted, nextIndex: 1, addresses: [ - { address: firstAddress, balance: "0.0000", tokenBalances: [] }, + { + address: firstAddress, + index: 0, + balance: "0.0000", + tokenBalances: [], + }, ], }; state.wallets.push(wallet); @@ -320,6 +331,7 @@ async function importXprvKey(ctx) { if (scan.addresses.length > 1) { wallet.addresses = scan.addresses.map((a) => ({ address: a.address, + index: a.index, balance: "0.0000", tokenBalances: [], })); diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js index 823e465..c5b8c5a 100644 --- a/src/popup/views/approval.js +++ b/src/popup/views/approval.js @@ -867,15 +867,15 @@ function gateOnWalletDefect(errorId, buttonId, address) { return true; } -// Locate the wallet and the address index owning an address. Returns null when -// no wallet holds it. Approvals look up the address they were raised for, not -// whichever address is active now: the approval named one account, and signing -// with another is what verification refuses. +// Locate the wallet and the stored address record owning an address. Returns +// null when no wallet holds it. Approvals look up the address they were raised +// for, not whichever address is active now: the approval named one account, +// and signing with another is what verification refuses. function findWalletFor(address) { for (const wallet of state.wallets) { for (let i = 0; i < wallet.addresses.length; i++) { if (wallet.addresses[i].address === address) { - return { wallet, addrIndex: i }; + return { wallet, addr: wallet.addresses[i] }; } } } @@ -1002,7 +1002,7 @@ function init(_ctx) { try { const signer = getSignerForAddress( active.wallet, - active.addrIndex, + active.addr, decryptedSecret, ); // Sign the approved transaction exactly as it was displayed. The @@ -1130,7 +1130,7 @@ function init(_ctx) { try { const signer = getSignerForAddress( active.wallet, - active.addrIndex, + active.addr, decryptedSecret, ); const sp = pendingSignParams; diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js index 30d1c4d..8aa76d1 100644 --- a/src/popup/views/confirmTx.js +++ b/src/popup/views/confirmTx.js @@ -583,7 +583,7 @@ function init(_ctx) { try { const signer = getSignerForAddress( wallet, - state.selectedAddress, + wallet.addresses[state.selectedAddress], decryptedSecret, ); const provider = getProvider(state.rpcUrl, state.networkId); diff --git a/src/popup/views/exportPrivkey.js b/src/popup/views/exportPrivkey.js index 3860387..fb40635 100644 --- a/src/popup/views/exportPrivkey.js +++ b/src/popup/views/exportPrivkey.js @@ -135,7 +135,18 @@ async function reveal() { // decrypt ran, the wipe has already happened, so the key is not // even derived, let alone written. if (!isCurrentReveal(generation)) return; - const signer = getSignerForAddress(wallet, addressIndex, secret); + let signer; + try { + signer = getSignerForAddress( + wallet, + wallet.addresses[addressIndex], + secret, + ); + } catch (e) { + // The password was right; it is the key that was refused. + showError("export-privkey-password-error", e.message); + return; + } $("export-privkey-password").value = ""; $("export-privkey-password-section").classList.add("hidden"); $("export-privkey-value").textContent = signer.privateKey; diff --git a/src/popup/views/home.js b/src/popup/views/home.js index b77d106..dc542bc 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -354,6 +354,7 @@ function render(ctx) { ); wallet.addresses.push({ address: newAddr, + index: wallet.nextIndex, balance: "0.0000", tokenBalances: [], }); diff --git a/src/shared/wallet.js b/src/shared/wallet.js index 16d9db0..d03e036 100644 --- a/src/shared/wallet.js +++ b/src/shared/wallet.js @@ -110,16 +110,21 @@ function addressFromPrivateKey(key) { return w.address; } -function getSignerForAddress(walletData, addrIndex, decryptedSecret) { +// The signer for `addr`, one of the wallet's stored address records. An HD or +// xprv address is derived at its own `index`, never at its position in the +// wallet's list: the two differ once an import scan passes over an unused +// index, or once an address that is not the last is removed. Whatever the +// wallet type, the key is refused unless it is the key of `addr.address`. +function getSignerForAddress(walletData, addr, decryptedSecret) { + let signer; if (walletData.type === "hd") { const node = HDNodeWallet.fromPhrase( decryptedSecret, "", BIP44_ETH_PATH, ); - return node.deriveChild(addrIndex); - } - if (walletData.type === "xprv") { + signer = node.deriveChild(addr.index); + } else if (walletData.type === "xprv") { // Checked here rather than through masterXprvOrThrow so the message // fits the situation: nobody is importing anything at signing time, // and this wallet is already in storage. src/shared/walletDefects.js @@ -137,9 +142,18 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) { "is not a master key, so it cannot sign.", ); } - return node.derivePath(BIP44_ETH_PATH).deriveChild(addrIndex); + signer = node.derivePath(BIP44_ETH_PATH).deriveChild(addr.index); + } else { + signer = new Wallet(decryptedSecret); } - return new Wallet(decryptedSecret); + if (signer.address.toLowerCase() !== addr.address.toLowerCase()) { + throw new Error( + "The key this wallet holds for " + + addr.address + + " belongs to a different address, so it was not used.", + ); + } + return signer; } function isValidMnemonic(mnemonic) { diff --git a/tests/addressKey.test.js b/tests/addressKey.test.js new file mode 100644 index 0000000..2a4ae79 --- /dev/null +++ b/tests/addressKey.test.js @@ -0,0 +1,386 @@ +// Sending, a site's signature request and private key export each use the key +// of the address they name: the key at the index that address was derived at, +// not the one at its position in the wallet's list. The two differ after an +// import whose scan passed over an unused index, and after an address that is +// not the last is removed (https://git.eeqj.de/sneak/AutistMask/issues/510). +// +// Driven through the real import, "+" button, address removal, confirmation +// screen, signature approval screen and export screen, with the node, the vault +// and the DOM stubbed. + +"use strict"; + +// The addresses the stub node reports a balance for, and the signed +// transactions it was handed. +const mockNode = { funded: new Set(), broadcast: [] }; + +jest.mock("ethers", () => { + const actual = jest.requireActual("ethers"); + class StubProvider { + async getBalance(address) { + return mockNode.funded.has(address) ? 1n : 0n; + } + async getTransactionCount() { + return 0; + } + async estimateGas() { + return 21000n; + } + async getFeeData() { + return { + maxFeePerGas: 20000000000n, + maxPriorityFeePerGas: 1000000000n, + gasPrice: 10000000000n, + }; + } + async getNetwork() { + return { chainId: 1n }; + } + async getCode() { + return "0x"; + } + async lookupAddress() { + return null; + } + async broadcastTransaction(signed) { + mockNode.broadcast.push(signed); + return { hash: "0x" + "ab".repeat(32) }; + } + } + return { ...actual, JsonRpcProvider: StubProvider }; +}); + +// No password is hashed: what the wallet stores is the phrase itself. +jest.mock("../src/shared/vault", () => ({ + encryptWithPassword: async (secret) => secret, + decryptWithPassword: async (secret) => secret, +})); + +// The wait screen polls for a receipt; these tests stop at the broadcast. +jest.mock("../src/popup/views/txStatus", () => ({ + showWait: jest.fn(), + showError: jest.fn(), +})); + +jest.mock("ethereum-blockies-base64", () => () => "data:image/png;base64,x"); + +global.fetch = jest.fn(() => { + throw new Error("tests must not perform network requests"); +}); + +const { + HDNodeWallet, + Transaction, + getBytes, + verifyMessage, +} = require("ethers"); + +// Publicly known development phrase; never fund it. +const PHRASE = "test test test test test test test test test test test junk"; +const PASSWORD = "correct horse battery staple"; +const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe"; +// "Hello", as the hex a page sends. +const MESSAGE = "0x48656c6c6f"; + +// The address and key at derivation index `index` of PHRASE, worked out with +// ethers directly rather than through the wallet's own code. +function derived(index) { + return HDNodeWallet.fromPhrase(PHRASE, "", "m/44'/60'/0'/0/" + index); +} + +// ------------------------------------------------------------ DOM stub + +function makeElement(id) { + const classes = new Set(); + const el = { + id, + textContent: "", + title: "", + value: "", + innerHTML: "", + disabled: false, + style: {}, + dataset: {}, + listeners: {}, + classList: { + add: (...names) => names.forEach((n) => classes.add(n)), + remove: (...names) => names.forEach((n) => classes.delete(n)), + contains: (n) => classes.has(n), + toggle: (n, force) => { + const on = force === undefined ? !classes.has(n) : force; + if (on) classes.add(n); + else classes.delete(n); + return on; + }, + }, + addEventListener: (name, fn) => { + el.listeners[name] = el.listeners[name] || []; + el.listeners[name].push(fn); + }, + querySelectorAll: () => [], + appendChild: () => {}, + }; + // Views reach for .parentElement to hide whole sections. + Object.defineProperty(el, "parentElement", { + get: () => node(id + "-parent"), + }); + return el; +} + +function makeDocument() { + const els = new Map(); + return { + getElementById(id) { + // The debug banner is created on demand by helpers.js; absent + // is the state a non-debug, non-testnet popup is in. + if (id === "debug-banner") return null; + if (!els.has(id)) els.set(id, makeElement(id)); + return els.get(id); + }, + createElement: () => makeElement("created"), + addEventListener: () => {}, + body: { prepend: () => {} }, + }; +} + +function node(id) { + return globalThis.document.getElementById(id); +} + +function click(element) { + const event = { stopPropagation: () => {} }; + return Promise.all((element.listeners.click || []).map((fn) => fn(event))); +} + +// show() starts the fee estimate without awaiting it; this lets it settle. +async function settle() { + for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0)); +} + +// ------------------------------------------------------------ harness + +globalThis.chrome = { + storage: { local: { get: async () => ({}), set: async () => {} } }, +}; + +const { state } = require("../src/shared/state"); +const { removeAddressFromState } = require("../src/shared/walletDelete"); +const addWallet = require("../src/popup/views/addWallet"); +const home = require("../src/popup/views/home"); +const confirmTx = require("../src/popup/views/confirmTx"); +const approval = require("../src/popup/views/approval"); +const exportPrivkey = require("../src/popup/views/exportPrivkey"); +const txStatus = require("../src/popup/views/txStatus"); + +// What the background answers when the approval window asks which approval it +// was opened for, and every message the popup sent it. +let approvalDetails; +let sent; + +// The "+" button of the wallet list's last render. Each render attaches the +// handler to the buttons it drew, as the browser's fresh list would have. +let plusButton; + +beforeEach(() => { + approvalDetails = null; + sent = []; + mockNode.broadcast = []; + globalThis.document = makeDocument(); + globalThis.window = { close: () => {} }; + globalThis.chrome = { + storage: { local: { get: async () => ({}), set: async () => {} } }, + runtime: { + connect: () => ({ postMessage: () => {} }), + sendMessage: (msg, reply) => { + sent.push(msg); + if (!reply) return; + reply( + msg.type === "AUTISTMASK_GET_APPROVAL" + ? approvalDetails + : null, + ); + }, + }, + }; + node("wallet-list").querySelectorAll = (selector) => { + if (selector !== ".btn-add-address") return []; + plusButton = makeElement("btn-add-address"); + plusButton.dataset.wallet = "0"; + return [plusButton]; + }; + // The explorer and price lookups these screens start fail offline, and + // say so; that is not under test. + for (const method of ["log", "warn", "error"]) { + jest.spyOn(console, method).mockImplementation(() => {}); + } + txStatus.showError.mockClear(); + + state.wallets = []; + state.rpcUrl = "https://rpc.example.invalid"; + state.networkId = "mainnet"; + state.viewStack = []; + state.currentView = "main"; + + addWallet.init({ + renderWalletList: () => {}, + doRefreshAndRender: () => {}, + pageClosed: new AbortController().signal, + }); + confirmTx.init({}); + approval.init({}); + exportPrivkey.init(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +// Import PHRASE on the add-wallet screen, with the stub node reporting a +// balance at each of `fundedIndices`, and wait for the address scan. +async function importPhrase(fundedIndices) { + mockNode.funded = new Set(fundedIndices.map((i) => derived(i).address)); + await click(node("tab-mnemonic")); + node("wallet-mnemonic").value = PHRASE; + node("add-wallet-password").value = PASSWORD; + node("add-wallet-password-confirm").value = PASSWORD; + await click(node("btn-add-wallet-confirm")); +} + +// Press the wallet's "+" button on Home. +async function addAddress() { + home.render({ + doRefreshAndRender: () => {}, + pageClosed: new AbortController().signal, + }); + await click(plusButton); +} + +// The key the export screen reveals for the address at `position`. +async function exportedKey(position) { + exportPrivkey.show(0, position); + node("export-privkey-password").value = PASSWORD; + await click(node("btn-export-privkey-confirm")); + return node("export-privkey-value").textContent; +} + +// Send from the address at `position`; resolves to the signed transaction the +// node was handed, or undefined when none was. +async function sendFrom(position) { + state.selectedWallet = 0; + state.selectedAddress = position; + confirmTx.show({ + from: state.wallets[0].addresses[position].address, + to: RECIPIENT, + amount: "0.5", + token: "ETH", + balance: "1.0", + }); + await settle(); + node("confirm-tx-password").value = PASSWORD; + await click(node("btn-confirm-send")); + return mockNode.broadcast.pop(); +} + +// A site asks the address at `position` to sign MESSAGE; resolves to the +// response the approval window sent the background. +async function signFor(position) { + const address = state.wallets[0].addresses[position].address; + approvalDetails = { + type: "sign", + origin: "https://dapp.example", + isPhishingDomain: false, + approvedFrom: address, + signParams: { + method: "personal_sign", + message: MESSAGE, + from: address, + }, + }; + await approval.show(1); + node("approve-sign-password").value = PASSWORD; + await click(node("btn-approve-sign")); + return sent.find((msg) => msg.type === "AUTISTMASK_SIGN_RESPONSE"); +} + +describe.each([ + [ + "an import whose scan passed over index 3", + async () => { + await importPhrase([0, 1, 2, 4]); + }, + // Address 4 on Home, derived at index 4. + { position: 3, index: 4 }, + ], + [ + "removing the middle one of three addresses", + async () => { + await importPhrase([0]); + await addAddress(); + await addAddress(); + removeAddressFromState(state, 0, 1); + }, + // Address 2 on Home after the removal, derived at index 2. + { position: 1, index: 2 }, + ], +])("after %s", (_name, setUp, { position, index }) => { + beforeEach(setUp); + + test("the wallet lists the address derived at that index there", () => { + expect(state.wallets[0].addresses[position].address).toBe( + derived(index).address, + ); + }); + + test("export reveals that address's key", async () => { + expect(await exportedKey(position)).toBe(derived(index).privateKey); + }); + + test("a send is signed by that address", async () => { + const signed = await sendFrom(position); + expect(Transaction.from(signed).from).toBe(derived(index).address); + }); + + test("a site's signature is made by that address", async () => { + const response = await signFor(position); + expect(verifyMessage(getBytes(MESSAGE), response.signature)).toBe( + derived(index).address, + ); + }); +}); + +describe("an address whose stored index derives a different address", () => { + function refusal() { + return ( + "The key this wallet holds for " + + derived(0).address + + " belongs to a different address, so it was not used." + ); + } + + beforeEach(async () => { + await importPhrase([0]); + state.wallets[0].addresses[0].index = 1; + }); + + test("export reveals nothing and says why", async () => { + expect(await exportedKey(0)).toBe(""); + expect(node("export-privkey-password-error").textContent).toBe( + refusal(), + ); + }); + + test("nothing is sent", async () => { + expect(await sendFrom(0)).toBeUndefined(); + expect(txStatus.showError).toHaveBeenCalledWith( + expect.anything(), + null, + refusal(), + ); + }); + + test("nothing is signed", async () => { + const response = await signFor(0); + expect(response.signature).toBeUndefined(); + expect(response.error).toBe(refusal()); + }); +}); diff --git a/tests/approvalNonce.test.js b/tests/approvalNonce.test.js index f23221d..7172a7c 100644 --- a/tests/approvalNonce.test.js +++ b/tests/approvalNonce.test.js @@ -136,7 +136,9 @@ async function openTxApproval(approvedTx) { xpub: "xpub-wallet-1", encryptedSecret: "encrypted-secret-1", nextIndex: 1, - addresses: [{ address: FROM, balance: "0", tokenBalances: [] }], + addresses: [ + { address: FROM, index: 0, balance: "0", tokenBalances: [] }, + ], }, ]; approval.init({}); diff --git a/tests/approvalVerify.test.js b/tests/approvalVerify.test.js index 6b2a13c..17206b7 100644 --- a/tests/approvalVerify.test.js +++ b/tests/approvalVerify.test.js @@ -1455,7 +1455,11 @@ describe("background population to popup signing to verification", () => { // What the popup does with it: signs it as given, populating nothing. async function popupSigns(approvedTx) { - const localSigner = getSignerForAddress(walletData, 0, SIGNER_KEY); + const localSigner = getSignerForAddress( + walletData, + { address: signer.address }, + SIGNER_KEY, + ); return localSigner.signTransaction({ ...approvedTx }); } @@ -1550,7 +1554,11 @@ describe("background population to popup signing to verification", () => { // address the user has switched to. test("the approved from stops the popup signing with another key", async () => { const approvedTx = await backgroundPrepares(TX_PARAMS); - const otherSigner = getSignerForAddress(walletData, 0, OTHER_KEY); + const otherSigner = getSignerForAddress( + walletData, + { address: other.address }, + OTHER_KEY, + ); await expect( otherSigner.signTransaction({ ...approvedTx }), ).rejects.toThrow(/from address mismatch/); diff --git a/tests/wallet.test.js b/tests/wallet.test.js index b9035df..0564b94 100644 --- a/tests/wallet.test.js +++ b/tests/wallet.test.js @@ -416,7 +416,7 @@ describe("extended key depth", () => { expect(() => wallet.getSignerForAddress( { type: "xprv" }, - 0, + { address: VECTOR_ADDRESSES[0], index: 0 }, accountXprv(VECTOR_PHRASE), ), ).toThrow(/master/i); @@ -504,7 +504,7 @@ describe("getSignerForAddress", () => { test.each([0, 1, 2])("hd wallet, address index %i", (index) => { const signer = wallet.getSignerForAddress( { type: "hd" }, - index, + { address: VECTOR_ADDRESSES[index], index }, VECTOR_PHRASE, ); @@ -515,7 +515,7 @@ describe("getSignerForAddress", () => { test.each([0, 1, 2])("xprv wallet, address index %i", (index) => { const signer = wallet.getSignerForAddress( { type: "xprv" }, - index, + { address: VECTOR_ADDRESSES[index], index }, masterXprv(VECTOR_PHRASE), ); @@ -527,7 +527,7 @@ describe("getSignerForAddress", () => { for (const index of [0, 1, 2]) { const signer = wallet.getSignerForAddress( { type: "privkey" }, - index, + { address: VECTOR_ADDRESSES[1], index }, VECTOR_PRIVATE_KEYS[1], ); @@ -535,10 +535,33 @@ describe("getSignerForAddress", () => { } }); + // https://git.eeqj.de/sneak/AutistMask/issues/510: an address whose stored + // index does not derive it, or a private key that is not its key. + test.each([ + ["hd", { address: VECTOR_ADDRESSES[2], index: 1 }, VECTOR_PHRASE], + [ + "xprv", + { address: VECTOR_ADDRESSES[2], index: 1 }, + masterXprv(VECTOR_PHRASE), + ], + ["privkey", { address: VECTOR_ADDRESSES[2] }, VECTOR_PRIVATE_KEYS[1]], + ])( + "%s wallet refuses a key that is not the address's own", + (type, addr, secret) => { + expect(() => + wallet.getSignerForAddress({ type }, addr, secret), + ).toThrow( + "The key this wallet holds for " + + VECTOR_ADDRESSES[2] + + " belongs to a different address, so it was not used.", + ); + }, + ); + test("the returned signer signs recoverably as the expected address", async () => { const signer = wallet.getSignerForAddress( { type: "hd" }, - 1, + { address: VECTOR_ADDRESSES[1], index: 1 }, VECTOR_PHRASE, ); const message = "AutistMask derivation test"; diff --git a/tests/walletDefects.test.js b/tests/walletDefects.test.js index a9fd35d..4b182d7 100644 --- a/tests/walletDefects.test.js +++ b/tests/walletDefects.test.js @@ -55,7 +55,7 @@ function healthyXprvWallet(name = "Master") { xpub, nextIndex: 1, encryptedSecret: "irrelevant-to-these-tests", - addresses: [{ address: firstAddress, balance: "0.0000" }], + addresses: [{ address: firstAddress, index: 0, balance: "0.0000" }], }; } @@ -558,7 +558,7 @@ describe("no path throws an unhandled error for a defective wallet", () => { try { wallet.getSignerForAddress( { type: "xprv" }, - 0, + brokenXprvWallet().addresses[0], accountXprv(VECTOR_PHRASE), ); } catch (e) { @@ -573,7 +573,7 @@ describe("no path throws an unhandled error for a defective wallet", () => { test("a healthy xprv wallet signs as it always did", () => { const signer = wallet.getSignerForAddress( { type: "xprv" }, - 0, + healthyXprvWallet().addresses[0], masterXprv(VECTOR_PHRASE), );