fix: explain a stored non-master xprv wallet instead of throwing at signing time (closes #234) #247
Reference in New Issue
Block a user
Delete Branch "fix/issue-234-non-master-xprv-wallet"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #234.
The problem
#210 made extended-key import refuse a non-master key. A
type: "xprv"wallet imported before that refusal existed still holds a key at depth > 0, and its addresses were derived by applying the Ethereum path beneath that key. Since #210 such a wallet throws insidegetSignerForAddress— on the send screen, after the user has typed a password, with no explanation.Detection without the password
An
xprvwallet stores the neutered BIP-44 Ethereum node, four levels below the key that was imported: the current path derives the absolutem/44'/60'/0'/0from a depth-0 key, and the pre-fix path derived the same four levels as a relative path beneath whatever depth it was given. A master import therefore stores a depth-4xpuband a depth-dimport stores depthd + 4, which makes the storedxpuban exact read on the imported key's depth — readable at wallet-list render time, with no decryption. Newsrc/shared/walletDefects.jsholds that check and the copy.An
xprvwallet whosexpubwill not parse gets no defect rather than a guessed one: there is no basis in that case to tell the user their key was not a master key.What the user sees
getSignerForAddressremains the backstop and now refuses with a full sentence fitted to signing rather than to import.Nothing is deleted or rewritten. The wallet, its addresses and its encrypted secret are untouched.
The copy
> This wallet's addresses were derived incorrectly.
>
> This wallet was imported from an extended private key that is not a master key. An earlier version applied the Ethereum derivation path beneath that key instead of from a master key, so the addresses listed here are not the ones that key produces under the standard path.
>
> Signing and sending are disabled for this wallet. The addresses do descend from the extended private key you imported, so anything they hold is still reachable by software that repeats the same non-standard derivation. Check them in a block explorer before deciding what to do.
>
> To see the addresses this key produces under the standard path, import the master extended private key, or the recovery phrase it came from, as a new wallet. Nothing here has been changed or removed, and this wallet stays until you delete it yourself.
The addresses genuinely descend from the key the user imported — the old code derived
44'/60'/0'/0beneath it, which is a real derivation from a real key — so the copy neither promises the funds are safe nor implies anything was lost. It says where the addresses came from, that this build cannot sign for them, that they are reachable by software repeating the same derivation, and that the wallet is not being deleted. Tests assert the absence of "safe", "lost", "stolen" and "gone", and the absence of "seed phrase", "mnemonic", "passphrase" and "account".Tests
tests/walletDefects.test.js, 22 tests. The fixtures are built the way each import path actually built them (the broken one viaHDNodeWallet.fromExtendedKey(accountXprv).derivePath("44'/60'/0'/0"), since the module under test now refuses that input), and two tests pin the resultingxpubdepths at 4 and 7 so the detection is anchored to real records rather than to an assumption.Demonstrated failing first: with
walletDefects.jsstubbed to returnnullunconditionally andwalletListHtmlabsent,make testreported 13 failed, 370 passed. After the fix, 383 passed.Verification
make checkgreen on the rebased branch: 15 suites, 383 tests passed,prettier --checkclean for bothlintandfmt-check.make buildalso run: 4 bundles emitted and verifiedautistmask-build-debug=off.make test-e2ewas not run; it needs a browser container and is not part ofmake check.PASS. Detection reproduced independently against real keys through both historical import paths (depth 0 stores xpub depth 4; depths 1/2/3/4/5 store 5/6/7/8/9 and are all detected; no false positive on any master import, and ethers refuses to serialize depth > 255 so there is no wrap-to-4 case); the copy's reachability claim is true — the stored addresses reproduce exactly from
HDNodeWallet.fromExtendedKey(xprv).derivePath("44'/60'/0'/0")and carry private keys.make check15 suites / 383 tests executed,make test-e2e13/13 in the pinned container,script/cibuildgreen uncached, fast-forwardable ontoorigin/nextatba35282.Disclosures and anomalies, none blocking:
home.jsbtn-send,addressDetail.jsbtn-send and btn-export-privkey,addressToken.jsbtn-address-token-send, andapproval.jsgateOnWalletDefectat both approval screens plus the two click-handler guards — leavesmake testat 15 suites / 383 passed, andmake test-e2ehas no defective-wallet case. OnlywalletListHtmland thegetSignerForAddressbackstop carry coverage. The gates are correct as written and e2e proves the four touched views still load, but they have no regression signal.7a7f9c5stored the xpub with no derivation at all, so a wallet written by that build stores depthd: a master import would be falsely condemned (depth 0) and a depth-4 import missed.7a7f9c5and its fix57959b7share a commit timestamp and no tag contains7a7f9c5, so this is unreachable from any release. Worth a clause in thewalletDefects.jscomment, which says "the pre-#210 path" as though there were one.addressDetail.jsbtn-export-privkey comment: "There is no private key to export for an address this wallet cannot derive." There is one — deriving it from the stored xprv works and yields the displayed address. The app declines to. The gate is still right (the export screen'scatchwould otherwise report "Wrong password."), but the comment states something untrue.showTxApprovaldoes not resetbtn-approve-tx.disabledbefore callinggateOnWalletDefect, whereshowSignApprovaldoes. Unreachable, since each approval gets its ownwindows.create()document, but the two screens now differ.0003a46adato0b2944a20e