fix: the first wallet's first address is active as soon as it is added (closes #512) #562
@@ -1544,8 +1544,14 @@ view would leave a wallet one click from deletion.
|
|||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Import" with a valid entry and a matching password of at least 12
|
- "Import" with a valid entry and a matching password of at least 12
|
||||||
characters → creates the wallet, clears the navigation stack, and →
|
characters → creates the wallet, clears the navigation stack, and →
|
||||||
**Home**. The phrase and xprv modes then scan for further used addresses
|
**Home**. When no address is active yet, as on the first wallet, the new
|
||||||
and report the count as a flash message.
|
wallet's first address becomes the active one. The phrase and xprv modes
|
||||||
|
then scan for further used addresses and report the count as a flash
|
||||||
|
message. When the scan finds two or more used addresses, they replace the
|
||||||
|
wallet's list, which then holds the first address only if it was used. If
|
||||||
|
the first address was active and is not in the new list, the new list's
|
||||||
|
first address becomes the active one and `AUTISTMASK_ACTIVE_CHANGED` is
|
||||||
|
broadcast.
|
||||||
- "Import" with a missing, short or mismatched password → full-sentence
|
- "Import" with a missing, short or mismatched password → full-sentence
|
||||||
error on the error line, no screen change
|
error on the error line, no screen change
|
||||||
- "Import" with an invalid entry or a duplicate wallet or address → flash
|
- "Import" with an invalid entry or a duplicate wallet or address → flash
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const {
|
|||||||
const { encryptWithPassword } = require("../../shared/vault");
|
const { encryptWithPassword } = require("../../shared/vault");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { scanForAddresses } = require("../../shared/balances");
|
const { scanForAddresses } = require("../../shared/balances");
|
||||||
|
const { notify } = require("../../shared/browserApi");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an address already exists in ANY wallet (hd, xprv, or key).
|
* Check if an address already exists in ANY wallet (hd, xprv, or key).
|
||||||
@@ -194,6 +195,9 @@ async function importMnemonic(ctx) {
|
|||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
|
if (state.activeAddress === null) {
|
||||||
|
state.activeAddress = firstAddress;
|
||||||
|
}
|
||||||
await saveState();
|
await saveState();
|
||||||
clearViewStack();
|
clearViewStack();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
@@ -215,8 +219,16 @@ async function importMnemonic(ctx) {
|
|||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
|
// The new list leaves out the first address when the scan found it
|
||||||
|
// unused. If it was the active one, the new list's first takes over,
|
||||||
|
// and the background is told as when the user picks one on Home.
|
||||||
|
const activeMoved =
|
||||||
|
state.activeAddress === firstAddress &&
|
||||||
|
wallet.addresses[0].address !== firstAddress;
|
||||||
|
if (activeMoved) state.activeAddress = wallet.addresses[0].address;
|
||||||
await saveState();
|
await saveState();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
|
if (activeMoved) notify({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||||
showFlash("Found " + scan.addresses.length + " addresses.");
|
showFlash("Found " + scan.addresses.length + " addresses.");
|
||||||
} else {
|
} else {
|
||||||
showFlash("Ready.", 1000);
|
showFlash("Ready.", 1000);
|
||||||
@@ -254,6 +266,9 @@ async function importPrivateKey(ctx) {
|
|||||||
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
||||||
});
|
});
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
|
if (state.activeAddress === null) {
|
||||||
|
state.activeAddress = addr;
|
||||||
|
}
|
||||||
await saveState();
|
await saveState();
|
||||||
clearViewStack();
|
clearViewStack();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
@@ -315,6 +330,9 @@ async function importXprvKey(ctx) {
|
|||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
|
if (state.activeAddress === null) {
|
||||||
|
state.activeAddress = firstAddress;
|
||||||
|
}
|
||||||
await saveState();
|
await saveState();
|
||||||
clearViewStack();
|
clearViewStack();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
@@ -336,8 +354,15 @@ async function importXprvKey(ctx) {
|
|||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
|
// As for a recovery phrase: an active first address the scan left
|
||||||
|
// out gives way to the new list's first.
|
||||||
|
const activeMoved =
|
||||||
|
state.activeAddress === firstAddress &&
|
||||||
|
wallet.addresses[0].address !== firstAddress;
|
||||||
|
if (activeMoved) state.activeAddress = wallet.addresses[0].address;
|
||||||
await saveState();
|
await saveState();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
|
if (activeMoved) notify({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||||
showFlash("Found " + scan.addresses.length + " addresses.");
|
showFlash("Found " + scan.addresses.length + " addresses.");
|
||||||
} else {
|
} else {
|
||||||
showFlash("Ready.", 1000);
|
showFlash("Ready.", 1000);
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Adding the first wallet left no address active until the popup was reopened,
|
||||||
|
// so Send and Receive on Home only said "No active address selected."
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/512). Each test adds a wallet
|
||||||
|
// through one of the three import tabs and checks which address is active by
|
||||||
|
// opening Receive from Home in the same popup.
|
||||||
|
|
||||||
|
const { HDNodeWallet, Mnemonic, Wallet } = require("ethers");
|
||||||
|
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
ADDRESS,
|
||||||
|
} = require("./support/popupBoot");
|
||||||
|
|
||||||
|
const PHRASE =
|
||||||
|
"abandon abandon abandon abandon abandon abandon " +
|
||||||
|
"abandon abandon abandon abandon abandon about";
|
||||||
|
const PRIVATE_KEY = "0x" + "11".repeat(32);
|
||||||
|
// The master key of PHRASE, so its first address is the phrase's.
|
||||||
|
const XPRV = HDNodeWallet.fromSeed(
|
||||||
|
Mnemonic.fromPhrase(PHRASE).computeSeed(),
|
||||||
|
).extendedKey;
|
||||||
|
const PASSWORD = "correct horse battery staple";
|
||||||
|
|
||||||
|
const PHRASE_ADDRESS = HDNodeWallet.fromPhrase(PHRASE).address;
|
||||||
|
|
||||||
|
// Addresses 1 and 2 of PHRASE used, address 0 not: a scan that answers these
|
||||||
|
// replaces the wallet's list with one that leaves out its first address.
|
||||||
|
const account = HDNodeWallet.fromPhrase(PHRASE, "", "m/44'/60'/0'/0");
|
||||||
|
const used = [1, 2].map((index) => ({
|
||||||
|
address: account.deriveChild(index).address,
|
||||||
|
index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{
|
||||||
|
tab: "tab-mnemonic",
|
||||||
|
field: "wallet-mnemonic",
|
||||||
|
secret: PHRASE,
|
||||||
|
address: PHRASE_ADDRESS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: "tab-privkey",
|
||||||
|
field: "import-private-key",
|
||||||
|
secret: PRIVATE_KEY,
|
||||||
|
address: new Wallet(PRIVATE_KEY).address,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: "tab-xprv",
|
||||||
|
field: "import-xprv-key",
|
||||||
|
secret: XPRV,
|
||||||
|
address: PHRASE_ADDRESS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fills in the open AddWallet screen and presses "Import".
|
||||||
|
async function importOn(page, { tab, field, secret }) {
|
||||||
|
await page.click(tab);
|
||||||
|
page.node(field).value = secret;
|
||||||
|
page.node("add-wallet-password").value = PASSWORD;
|
||||||
|
page.node("add-wallet-password-confirm").value = PASSWORD;
|
||||||
|
await page.click("btn-add-wallet-confirm");
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the first wallet's first address is active right away", () => {
|
||||||
|
test.each(TABS)("added from $tab, Receive opens on it", async (entry) => {
|
||||||
|
const page = await bootPopup(undefined);
|
||||||
|
expect(page.pageErrors).toEqual([]);
|
||||||
|
await page.click("btn-welcome-add");
|
||||||
|
await importOn(page, entry);
|
||||||
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
|
|
||||||
|
await page.click("btn-main-receive");
|
||||||
|
expect(page.visibleViews()).toEqual(["receive"]);
|
||||||
|
expect(page.node("receive-address-block").dataset.full).toBe(
|
||||||
|
entry.address,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a scan that leaves out the first address", () => {
|
||||||
|
// The key tab runs no scan.
|
||||||
|
test.each(TABS.filter((t) => t.tab !== "tab-privkey"))(
|
||||||
|
"added from $tab, the scan's first address is active",
|
||||||
|
async (entry) => {
|
||||||
|
const page = await bootPopup(undefined);
|
||||||
|
const { scanForAddresses } = require("../src/shared/balances");
|
||||||
|
scanForAddresses.mockResolvedValueOnce({
|
||||||
|
addresses: used,
|
||||||
|
nextIndex: 3,
|
||||||
|
});
|
||||||
|
await page.click("btn-welcome-add");
|
||||||
|
await importOn(page, entry);
|
||||||
|
|
||||||
|
expect(page.storage.read("autistmask").activeAddress).toBe(
|
||||||
|
used[0].address,
|
||||||
|
);
|
||||||
|
expect(globalThis.chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||||
|
type: "AUTISTMASK_ACTIVE_CHANGED",
|
||||||
|
});
|
||||||
|
await page.click("btn-main-receive");
|
||||||
|
expect(page.node("receive-address-block").dataset.full).toBe(
|
||||||
|
used[0].address,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adding a wallet while an address is active", () => {
|
||||||
|
test.each(TABS)(
|
||||||
|
"added from $tab, the active address is unchanged",
|
||||||
|
async (entry) => {
|
||||||
|
const page = await bootPopup(unversionedValidProfile());
|
||||||
|
expect(page.pageErrors).toEqual([]);
|
||||||
|
// The phrase and xprv scans leave out the new wallet's first
|
||||||
|
// address, which was never active here. The key tab runs no scan.
|
||||||
|
const { scanForAddresses } = require("../src/shared/balances");
|
||||||
|
scanForAddresses.mockResolvedValueOnce({
|
||||||
|
addresses: used,
|
||||||
|
nextIndex: 3,
|
||||||
|
});
|
||||||
|
await page.click("btn-add-wallet-bottom");
|
||||||
|
await importOn(page, entry);
|
||||||
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
|
|
||||||
|
expect(page.storage.read("autistmask").activeAddress).toBe(ADDRESS);
|
||||||
|
expect(
|
||||||
|
globalThis.chrome.runtime.sendMessage,
|
||||||
|
).not.toHaveBeenCalledWith({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||||
|
await page.click("btn-main-receive");
|
||||||
|
expect(page.node("receive-address-block").dataset.full).toBe(
|
||||||
|
ADDRESS,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -265,12 +265,17 @@ async function bootPopup(stored, options) {
|
|||||||
formatAddressTotal: () => "",
|
formatAddressTotal: () => "",
|
||||||
getAddressValue: () => ({ usd: null, partial: false }),
|
getAddressValue: () => ({ usd: null, partial: false }),
|
||||||
}));
|
}));
|
||||||
|
// scanForAddresses() answers in the real one's shape, a scan that found
|
||||||
|
// no used address: AddWallet reads `addresses` from it.
|
||||||
jest.doMock("../../src/shared/balances", () => ({
|
jest.doMock("../../src/shared/balances", () => ({
|
||||||
fetchTokenBalances: jest.fn(async () => []),
|
fetchTokenBalances: jest.fn(async () => []),
|
||||||
refreshBalances: jest.fn(async () => {}),
|
refreshBalances: jest.fn(async () => {}),
|
||||||
lookupTokenInfo: jest.fn(async () => null),
|
lookupTokenInfo: jest.fn(async () => null),
|
||||||
getProvider: () => ({}),
|
getProvider: () => ({}),
|
||||||
scanForAddresses: jest.fn(async () => []),
|
scanForAddresses: jest.fn(async () => ({
|
||||||
|
addresses: [],
|
||||||
|
nextIndex: 1,
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
// filterTransactions() answers in the real one's shape: Home,
|
// filterTransactions() answers in the real one's shape: Home,
|
||||||
// AddressDetail and AddressToken read both fields, and a bare list makes
|
// AddressDetail and AddressToken read both fields, and a bare list makes
|
||||||
|
|||||||
Reference in New Issue
Block a user