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 "+"). getSignerForAddress() derives at that index, refuses a record without one, and refuses any key whose address differs from the record's. The confirmation screen signs as the From address it shows, not the selected one. The export screen reports a refusal instead of calling the password wrong. Model: opus-5-5
This commit is contained in:
@@ -1192,7 +1192,16 @@ The core hierarchy is **Wallets → Addresses**:
|
|||||||
of its stored `xpub` by `src/shared/walletDefects.js`, explained in the
|
of its stored `xpub` by `src/shared/walletDefects.js`, explained in the
|
||||||
wallet list, and blocked from signing, sending and private-key export. It
|
wallet list, and blocked from signing, sending and private-key export. It
|
||||||
is never deleted or rewritten.
|
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;
|
||||||
|
an HD or xprv address with no valid `index` is refused before anything is
|
||||||
|
derived (`getSignerForAddress()` in `src/shared/wallet.js`). Each refusal is a
|
||||||
|
full sentence naming the address.
|
||||||
- The user can have multiple wallets, each with multiple addresses (HD) or a
|
- The user can have multiple wallets, each with multiple addresses (HD) or a
|
||||||
single address (key).
|
single address (key).
|
||||||
|
|
||||||
@@ -1518,6 +1527,9 @@ view would leave a wallet one click from deletion.
|
|||||||
change)
|
change)
|
||||||
- "Reveal" (wrong password) → full-sentence error on the error line, nothing
|
- "Reveal" (wrong password) → full-sentence error on the error line, nothing
|
||||||
revealed (no screen change)
|
revealed (no screen change)
|
||||||
|
- "Reveal" (correct password, but this address's key is refused, see Data
|
||||||
|
Model) → full-sentence error saying why, nothing revealed (no screen
|
||||||
|
change)
|
||||||
- "Back" → previous screen (AddressDetail)
|
- "Back" → previous screen (AddressDetail)
|
||||||
- Settings gear → **Settings**, whose "Back" goes to AddressDetail: leaving
|
- Settings gear → **Settings**, whose "Back" goes to AddressDetail: leaving
|
||||||
drops the address the screen was showing, so it also takes the screen off
|
drops the address the screen was showing, so it also takes the screen off
|
||||||
@@ -1653,6 +1665,13 @@ view would leave a wallet one click from deletion.
|
|||||||
be signed for another
|
be signed for another
|
||||||
- "Sign & Send" (wrong password) → "Wrong password." on the password error
|
- "Sign & Send" (wrong password) → "Wrong password." on the password error
|
||||||
line, no screen change
|
line, no screen change
|
||||||
|
- "Sign & Send" signs as the From address above, not as whichever address is
|
||||||
|
selected: the two are saved separately, so the popup can reopen onto this
|
||||||
|
screen with another address selected. When the selected wallet does not
|
||||||
|
hold the From address → full-sentence error on the password error line,
|
||||||
|
nothing sent (no screen change)
|
||||||
|
- "Sign & Send" (correct password, but the From address's key is refused,
|
||||||
|
see Data Model) → nothing is signed → **ErrorTx** saying why
|
||||||
- "Back" → **Send**
|
- "Back" → **Send**
|
||||||
|
|
||||||
#### WaitTx (`wait-tx`)
|
#### WaitTx (`wait-tx`)
|
||||||
@@ -2118,6 +2137,9 @@ view would leave a wallet one click from deletion.
|
|||||||
shown, exactly as shown, hands the signed transaction to the background to
|
shown, exactly as shown, hands the signed transaction to the background to
|
||||||
broadcast, then → **WaitTx** in the same popup window
|
broadcast, then → **WaitTx** in the same popup window
|
||||||
- "Confirm" (wrong password) → error line, no screen change
|
- "Confirm" (wrong password) → error line, no screen change
|
||||||
|
- "Confirm" (correct password, but the From address's key is refused, see
|
||||||
|
Data Model) → full-sentence error on the error line saying why, nothing
|
||||||
|
signed (no screen change)
|
||||||
- "Reject" → closes popup (returns rejection to background)
|
- "Reject" → closes popup (returns rejection to background)
|
||||||
- Popup window closed without answering → the request is rejected with
|
- Popup window closed without answering → the request is rejected with
|
||||||
EIP-1193 code 4001
|
EIP-1193 code 4001
|
||||||
|
|||||||
@@ -184,7 +184,12 @@ async function importMnemonic(ctx) {
|
|||||||
encryptedSecret: encrypted,
|
encryptedSecret: encrypted,
|
||||||
nextIndex: 1,
|
nextIndex: 1,
|
||||||
addresses: [
|
addresses: [
|
||||||
{ address: firstAddress, balance: "0.0000", tokenBalances: [] },
|
{
|
||||||
|
address: firstAddress,
|
||||||
|
index: 0,
|
||||||
|
balance: "0.0000",
|
||||||
|
tokenBalances: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
@@ -205,6 +210,7 @@ async function importMnemonic(ctx) {
|
|||||||
if (scan.addresses.length > 1) {
|
if (scan.addresses.length > 1) {
|
||||||
wallet.addresses = scan.addresses.map((a) => ({
|
wallet.addresses = scan.addresses.map((a) => ({
|
||||||
address: a.address,
|
address: a.address,
|
||||||
|
index: a.index,
|
||||||
balance: "0.0000",
|
balance: "0.0000",
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
@@ -299,7 +305,12 @@ async function importXprvKey(ctx) {
|
|||||||
encryptedSecret: encrypted,
|
encryptedSecret: encrypted,
|
||||||
nextIndex: 1,
|
nextIndex: 1,
|
||||||
addresses: [
|
addresses: [
|
||||||
{ address: firstAddress, balance: "0.0000", tokenBalances: [] },
|
{
|
||||||
|
address: firstAddress,
|
||||||
|
index: 0,
|
||||||
|
balance: "0.0000",
|
||||||
|
tokenBalances: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
@@ -320,6 +331,7 @@ async function importXprvKey(ctx) {
|
|||||||
if (scan.addresses.length > 1) {
|
if (scan.addresses.length > 1) {
|
||||||
wallet.addresses = scan.addresses.map((a) => ({
|
wallet.addresses = scan.addresses.map((a) => ({
|
||||||
address: a.address,
|
address: a.address,
|
||||||
|
index: a.index,
|
||||||
balance: "0.0000",
|
balance: "0.0000",
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ function init(_ctx) {
|
|||||||
// This address's private key can be derived from the stored key,
|
// This address's private key can be derived from the stored key,
|
||||||
// but export goes through getSignerForAddress, which refuses a key
|
// but export goes through getSignerForAddress, which refuses a key
|
||||||
// that is not a master key. Without this the export screen would
|
// that is not a master key. Without this the export screen would
|
||||||
// take a password and then report that refusal as a wrong password.
|
// take a password and then refuse.
|
||||||
const defect = selectedWalletDefect();
|
const defect = selectedWalletDefect();
|
||||||
if (defect) {
|
if (defect) {
|
||||||
showFlash(defect.shortMessage);
|
showFlash(defect.shortMessage);
|
||||||
|
|||||||
@@ -913,15 +913,15 @@ function gateOnWalletDefect(errorId, buttonId, address) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locate the wallet and the address index owning an address. Returns null when
|
// Locate the wallet and the stored address record owning an address. Returns
|
||||||
// no wallet holds it. Approvals look up the address they were raised for, not
|
// null when no wallet holds it. Approvals look up the address they were raised
|
||||||
// whichever address is active now: the approval named one account, and signing
|
// for, not whichever address is active now: the approval named one account,
|
||||||
// with another is what verification refuses.
|
// and signing with another is what verification refuses.
|
||||||
function findWalletFor(address) {
|
function findWalletFor(address) {
|
||||||
for (const wallet of state.wallets) {
|
for (const wallet of state.wallets) {
|
||||||
for (let i = 0; i < wallet.addresses.length; i++) {
|
for (let i = 0; i < wallet.addresses.length; i++) {
|
||||||
if (wallet.addresses[i].address === address) {
|
if (wallet.addresses[i].address === address) {
|
||||||
return { wallet, addrIndex: i };
|
return { wallet, addr: wallet.addresses[i] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1048,7 +1048,7 @@ function init(_ctx) {
|
|||||||
try {
|
try {
|
||||||
const signer = getSignerForAddress(
|
const signer = getSignerForAddress(
|
||||||
active.wallet,
|
active.wallet,
|
||||||
active.addrIndex,
|
active.addr,
|
||||||
decryptedSecret,
|
decryptedSecret,
|
||||||
);
|
);
|
||||||
// Sign the approved transaction exactly as it was displayed. The
|
// Sign the approved transaction exactly as it was displayed. The
|
||||||
@@ -1176,7 +1176,7 @@ function init(_ctx) {
|
|||||||
try {
|
try {
|
||||||
const signer = getSignerForAddress(
|
const signer = getSignerForAddress(
|
||||||
active.wallet,
|
active.wallet,
|
||||||
active.addrIndex,
|
active.addr,
|
||||||
decryptedSecret,
|
decryptedSecret,
|
||||||
);
|
);
|
||||||
const sp = pendingSignParams;
|
const sp = pendingSignParams;
|
||||||
|
|||||||
@@ -567,6 +567,19 @@ function init(_ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sign as the From address this screen shows. The selection is saved
|
||||||
|
// apart from this screen, so the popup can reopen onto it with another
|
||||||
|
// address selected.
|
||||||
|
const addr = wallet.addresses.find((a) => a.address === pendingTx.from);
|
||||||
|
if (!addr) {
|
||||||
|
showError(
|
||||||
|
"confirm-tx-password-error",
|
||||||
|
"The selected wallet does not hold the From address above, " +
|
||||||
|
"so nothing was sent. Please go back and send again.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const password = $("confirm-tx-password").value;
|
const password = $("confirm-tx-password").value;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
showError(
|
showError(
|
||||||
@@ -597,11 +610,7 @@ function init(_ctx) {
|
|||||||
|
|
||||||
let tx;
|
let tx;
|
||||||
try {
|
try {
|
||||||
const signer = getSignerForAddress(
|
const signer = getSignerForAddress(wallet, addr, decryptedSecret);
|
||||||
wallet,
|
|
||||||
state.selectedAddress,
|
|
||||||
decryptedSecret,
|
|
||||||
);
|
|
||||||
const provider = getProvider(state.rpcUrl, state.networkId);
|
const provider = getProvider(state.rpcUrl, state.networkId);
|
||||||
const connectedSigner = signer.connect(provider);
|
const connectedSigner = signer.connect(provider);
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,18 @@ async function reveal() {
|
|||||||
// decrypt ran, the wipe has already happened, so the key is not
|
// decrypt ran, the wipe has already happened, so the key is not
|
||||||
// even derived, let alone written.
|
// even derived, let alone written.
|
||||||
if (!isCurrentReveal(generation)) return;
|
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").value = "";
|
||||||
$("export-privkey-password-section").classList.add("hidden");
|
$("export-privkey-password-section").classList.add("hidden");
|
||||||
$("export-privkey-value").textContent = signer.privateKey;
|
$("export-privkey-value").textContent = signer.privateKey;
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ function render(ctx) {
|
|||||||
);
|
);
|
||||||
wallet.addresses.push({
|
wallet.addresses.push({
|
||||||
address: newAddr,
|
address: newAddr,
|
||||||
|
index: wallet.nextIndex,
|
||||||
balance: "0.0000",
|
balance: "0.0000",
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
});
|
});
|
||||||
|
|||||||
+32
-6
@@ -110,16 +110,33 @@ function addressFromPrivateKey(key) {
|
|||||||
return w.address;
|
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. An HD or xprv
|
||||||
|
// record without a valid `index` is refused before anything is derived.
|
||||||
|
// Whatever the wallet type, the key is refused unless it is the key of
|
||||||
|
// `addr.address`.
|
||||||
|
function getSignerForAddress(walletData, addr, decryptedSecret) {
|
||||||
|
if (
|
||||||
|
(walletData.type === "hd" || walletData.type === "xprv") &&
|
||||||
|
!(Number.isInteger(addr.index) && addr.index >= 0)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"No derivation index is stored for " +
|
||||||
|
addr.address +
|
||||||
|
", so its key cannot be derived.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let signer;
|
||||||
if (walletData.type === "hd") {
|
if (walletData.type === "hd") {
|
||||||
const node = HDNodeWallet.fromPhrase(
|
const node = HDNodeWallet.fromPhrase(
|
||||||
decryptedSecret,
|
decryptedSecret,
|
||||||
"",
|
"",
|
||||||
BIP44_ETH_PATH,
|
BIP44_ETH_PATH,
|
||||||
);
|
);
|
||||||
return node.deriveChild(addrIndex);
|
signer = node.deriveChild(addr.index);
|
||||||
}
|
} else if (walletData.type === "xprv") {
|
||||||
if (walletData.type === "xprv") {
|
|
||||||
// Checked here rather than through masterXprvOrThrow so the message
|
// Checked here rather than through masterXprvOrThrow so the message
|
||||||
// fits the situation: nobody is importing anything at signing time,
|
// fits the situation: nobody is importing anything at signing time,
|
||||||
// and this wallet is already in storage. src/shared/walletDefects.js
|
// and this wallet is already in storage. src/shared/walletDefects.js
|
||||||
@@ -137,9 +154,18 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
|
|||||||
"is not a master key, so it cannot sign.",
|
"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) {
|
function isValidMnemonic(mnemonic) {
|
||||||
|
|||||||
@@ -0,0 +1,490 @@
|
|||||||
|
// Sending, a site's transaction and signature requests 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). A send is signed by the
|
||||||
|
// From address its confirmation screen shows, whichever address is selected.
|
||||||
|
//
|
||||||
|
// Driven through the real import, "+" button, address removal, confirmation
|
||||||
|
// screen, transaction and signature approval screens 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,
|
||||||
|
Mnemonic,
|
||||||
|
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";
|
||||||
|
// PHRASE's master extended private key, as the xprv import takes it.
|
||||||
|
const XPRV = HDNodeWallet.fromSeed(
|
||||||
|
Mnemonic.fromPhrase(PHRASE).computeSeed(),
|
||||||
|
).extendedKey;
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same, importing XPRV on the add-wallet screen's xprv tab.
|
||||||
|
async function importXprv(fundedIndices) {
|
||||||
|
mockNode.funded = new Set(fundedIndices.map((i) => derived(i).address));
|
||||||
|
await click(node("tab-xprv"));
|
||||||
|
node("import-xprv-key").value = XPRV;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm a send whose From line shows `from`, with the address at position
|
||||||
|
// `selected` selected; resolves to the signed transaction the node was handed,
|
||||||
|
// or undefined when none was.
|
||||||
|
async function confirmSend(from, selected) {
|
||||||
|
state.selectedWallet = 0;
|
||||||
|
state.selectedAddress = selected;
|
||||||
|
confirmTx.show({
|
||||||
|
from,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send from the address at `position`, with that address selected, as the Send
|
||||||
|
// button leaves it.
|
||||||
|
function sendFrom(position) {
|
||||||
|
return confirmSend(state.wallets[0].addresses[position].address, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A site asks the address at `position` to send RECIPIENT a transaction of no
|
||||||
|
// value; resolves to the response the approval window sent the background.
|
||||||
|
async function transactFor(position) {
|
||||||
|
const address = state.wallets[0].addresses[position].address;
|
||||||
|
approvalDetails = {
|
||||||
|
type: "tx",
|
||||||
|
origin: "https://dapp.example",
|
||||||
|
isPhishingDomain: false,
|
||||||
|
approvedFrom: address,
|
||||||
|
approvedTx: {
|
||||||
|
type: 2,
|
||||||
|
from: address,
|
||||||
|
chainId: "0x1",
|
||||||
|
nonce: "0x0",
|
||||||
|
gasLimit: "0x5208",
|
||||||
|
maxPriorityFeePerGas: "0x3b9aca00",
|
||||||
|
maxFeePerGas: "0x77359400",
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: "0x0",
|
||||||
|
data: "0x",
|
||||||
|
accessList: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await approval.show(1);
|
||||||
|
node("approve-tx-password").value = PASSWORD;
|
||||||
|
await click(node("btn-approve-tx"));
|
||||||
|
return sent.find((msg) => msg.type === "AUTISTMASK_TX_RESPONSE");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
[
|
||||||
|
"a recovery phrase import whose scan found only the first address",
|
||||||
|
async () => {
|
||||||
|
await importPhrase([0]);
|
||||||
|
},
|
||||||
|
// The address the import stores before its scan.
|
||||||
|
{ position: 0, index: 0 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"an xprv import whose scan found only the first address",
|
||||||
|
async () => {
|
||||||
|
await importXprv([0]);
|
||||||
|
},
|
||||||
|
{ position: 0, index: 0 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"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 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"an xprv import whose scan passed over index 3",
|
||||||
|
async () => {
|
||||||
|
await importXprv([0, 1, 2, 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a site's transaction is signed by that address", async () => {
|
||||||
|
const response = await transactFor(position);
|
||||||
|
expect(Transaction.from(response.rawSignedTx).from).toBe(
|
||||||
|
derived(index).address,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// The selection and the confirmation screen are saved apart, so the popup can
|
||||||
|
// reopen onto the screen with another address selected than its From line
|
||||||
|
// shows.
|
||||||
|
describe("a confirmation screen whose From is not the selected address", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await importPhrase([0, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the send is signed by the From address shown", async () => {
|
||||||
|
const signed = await confirmSend(derived(1).address, 0);
|
||||||
|
expect(Transaction.from(signed).from).toBe(derived(1).address);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nothing is sent when the selected wallet does not hold it", async () => {
|
||||||
|
expect(await confirmSend(derived(5).address, 0)).toBeUndefined();
|
||||||
|
expect(node("confirm-tx-password-error").textContent).toBe(
|
||||||
|
"The selected wallet does not hold the From address above, " +
|
||||||
|
"so nothing was sent. Please go back and send again.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -136,7 +136,9 @@ async function openTxApproval(approvedTx) {
|
|||||||
xpub: "xpub-wallet-1",
|
xpub: "xpub-wallet-1",
|
||||||
encryptedSecret: "encrypted-secret-1",
|
encryptedSecret: "encrypted-secret-1",
|
||||||
nextIndex: 1,
|
nextIndex: 1,
|
||||||
addresses: [{ address: FROM, balance: "0", tokenBalances: [] }],
|
addresses: [
|
||||||
|
{ address: FROM, index: 0, balance: "0", tokenBalances: [] },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
approval.init({});
|
approval.init({});
|
||||||
|
|||||||
@@ -1454,7 +1454,11 @@ describe("background population to popup signing to verification", () => {
|
|||||||
|
|
||||||
// What the popup does with it: signs it as given, populating nothing.
|
// What the popup does with it: signs it as given, populating nothing.
|
||||||
async function popupSigns(approvedTx) {
|
async function popupSigns(approvedTx) {
|
||||||
const localSigner = getSignerForAddress(walletData, 0, SIGNER_KEY);
|
const localSigner = getSignerForAddress(
|
||||||
|
walletData,
|
||||||
|
{ address: signer.address },
|
||||||
|
SIGNER_KEY,
|
||||||
|
);
|
||||||
return localSigner.signTransaction({ ...approvedTx });
|
return localSigner.signTransaction({ ...approvedTx });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1549,7 +1553,11 @@ describe("background population to popup signing to verification", () => {
|
|||||||
// address the user has switched to.
|
// address the user has switched to.
|
||||||
test("the approved from stops the popup signing with another key", async () => {
|
test("the approved from stops the popup signing with another key", async () => {
|
||||||
const approvedTx = await backgroundPrepares(TX_PARAMS);
|
const approvedTx = await backgroundPrepares(TX_PARAMS);
|
||||||
const otherSigner = getSignerForAddress(walletData, 0, OTHER_KEY);
|
const otherSigner = getSignerForAddress(
|
||||||
|
walletData,
|
||||||
|
{ address: other.address },
|
||||||
|
OTHER_KEY,
|
||||||
|
);
|
||||||
await expect(
|
await expect(
|
||||||
otherSigner.signTransaction({ ...approvedTx }),
|
otherSigner.signTransaction({ ...approvedTx }),
|
||||||
).rejects.toThrow(/from address mismatch/);
|
).rejects.toThrow(/from address mismatch/);
|
||||||
|
|||||||
+49
-5
@@ -416,7 +416,7 @@ describe("extended key depth", () => {
|
|||||||
expect(() =>
|
expect(() =>
|
||||||
wallet.getSignerForAddress(
|
wallet.getSignerForAddress(
|
||||||
{ type: "xprv" },
|
{ type: "xprv" },
|
||||||
0,
|
{ address: VECTOR_ADDRESSES[0], index: 0 },
|
||||||
accountXprv(VECTOR_PHRASE),
|
accountXprv(VECTOR_PHRASE),
|
||||||
),
|
),
|
||||||
).toThrow(/master/i);
|
).toThrow(/master/i);
|
||||||
@@ -504,7 +504,7 @@ describe("getSignerForAddress", () => {
|
|||||||
test.each([0, 1, 2])("hd wallet, address index %i", (index) => {
|
test.each([0, 1, 2])("hd wallet, address index %i", (index) => {
|
||||||
const signer = wallet.getSignerForAddress(
|
const signer = wallet.getSignerForAddress(
|
||||||
{ type: "hd" },
|
{ type: "hd" },
|
||||||
index,
|
{ address: VECTOR_ADDRESSES[index], index },
|
||||||
VECTOR_PHRASE,
|
VECTOR_PHRASE,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -515,7 +515,7 @@ describe("getSignerForAddress", () => {
|
|||||||
test.each([0, 1, 2])("xprv wallet, address index %i", (index) => {
|
test.each([0, 1, 2])("xprv wallet, address index %i", (index) => {
|
||||||
const signer = wallet.getSignerForAddress(
|
const signer = wallet.getSignerForAddress(
|
||||||
{ type: "xprv" },
|
{ type: "xprv" },
|
||||||
index,
|
{ address: VECTOR_ADDRESSES[index], index },
|
||||||
masterXprv(VECTOR_PHRASE),
|
masterXprv(VECTOR_PHRASE),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -527,7 +527,7 @@ describe("getSignerForAddress", () => {
|
|||||||
for (const index of [0, 1, 2]) {
|
for (const index of [0, 1, 2]) {
|
||||||
const signer = wallet.getSignerForAddress(
|
const signer = wallet.getSignerForAddress(
|
||||||
{ type: "privkey" },
|
{ type: "privkey" },
|
||||||
index,
|
{ address: VECTOR_ADDRESSES[1], index },
|
||||||
VECTOR_PRIVATE_KEYS[1],
|
VECTOR_PRIVATE_KEYS[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -535,10 +535,54 @@ 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.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// An HD or xprv address record without a usable `index` is refused in a
|
||||||
|
// sentence, not with what ethers says about the value it was handed.
|
||||||
|
test.each([
|
||||||
|
["hd", VECTOR_PHRASE],
|
||||||
|
["xprv", masterXprv(VECTOR_PHRASE)],
|
||||||
|
])("%s wallet refuses an address with no valid index", (type, secret) => {
|
||||||
|
for (const index of [undefined, null, -1, 1.5, "1"]) {
|
||||||
|
expect(() =>
|
||||||
|
wallet.getSignerForAddress(
|
||||||
|
{ type },
|
||||||
|
{ address: VECTOR_ADDRESSES[1], index },
|
||||||
|
secret,
|
||||||
|
),
|
||||||
|
).toThrow(
|
||||||
|
"No derivation index is stored for " +
|
||||||
|
VECTOR_ADDRESSES[1] +
|
||||||
|
", so its key cannot be derived.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("the returned signer signs recoverably as the expected address", async () => {
|
test("the returned signer signs recoverably as the expected address", async () => {
|
||||||
const signer = wallet.getSignerForAddress(
|
const signer = wallet.getSignerForAddress(
|
||||||
{ type: "hd" },
|
{ type: "hd" },
|
||||||
1,
|
{ address: VECTOR_ADDRESSES[1], index: 1 },
|
||||||
VECTOR_PHRASE,
|
VECTOR_PHRASE,
|
||||||
);
|
);
|
||||||
const message = "AutistMask derivation test";
|
const message = "AutistMask derivation test";
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function healthyXprvWallet(name = "Master") {
|
|||||||
xpub,
|
xpub,
|
||||||
nextIndex: 1,
|
nextIndex: 1,
|
||||||
encryptedSecret: "irrelevant-to-these-tests",
|
encryptedSecret: "irrelevant-to-these-tests",
|
||||||
addresses: [{ address: firstAddress, balance: "0.0000" }],
|
addresses: [{ address: firstAddress, index: 0, balance: "0.0000" }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,11 @@ function brokenXprvWallet(name = "Imported xprv") {
|
|||||||
nextIndex: 1,
|
nextIndex: 1,
|
||||||
encryptedSecret: "irrelevant-to-these-tests",
|
encryptedSecret: "irrelevant-to-these-tests",
|
||||||
addresses: [
|
addresses: [
|
||||||
{ address: node.deriveChild(0).address, balance: "0.0000" },
|
{
|
||||||
|
address: node.deriveChild(0).address,
|
||||||
|
index: 0,
|
||||||
|
balance: "0.0000",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -570,7 +574,7 @@ describe("no path throws an unhandled error for a defective wallet", () => {
|
|||||||
try {
|
try {
|
||||||
wallet.getSignerForAddress(
|
wallet.getSignerForAddress(
|
||||||
{ type: "xprv" },
|
{ type: "xprv" },
|
||||||
0,
|
brokenXprvWallet().addresses[0],
|
||||||
accountXprv(VECTOR_PHRASE),
|
accountXprv(VECTOR_PHRASE),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -585,7 +589,7 @@ describe("no path throws an unhandled error for a defective wallet", () => {
|
|||||||
test("a healthy xprv wallet signs as it always did", () => {
|
test("a healthy xprv wallet signs as it always did", () => {
|
||||||
const signer = wallet.getSignerForAddress(
|
const signer = wallet.getSignerForAddress(
|
||||||
{ type: "xprv" },
|
{ type: "xprv" },
|
||||||
0,
|
healthyXprvWallet().addresses[0],
|
||||||
masterXprv(VECTOR_PHRASE),
|
masterXprv(VECTOR_PHRASE),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user