-
Notifications
You must be signed in to change notification settings - Fork 191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: wallet state corruption round 4 #8519
base: develop
Are you sure you want to change the base?
Conversation
@@ -63,14 +61,11 @@ export const WalletViewsSwitch = () => { | |||
if (disposition === 'initializing' || disposition === 'recovering') { | |||
await wallet?.cancel() | |||
disconnect() | |||
store.dispatch(localWalletSlice.actions.clearLocalWallet()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
disconnect()
calls RESET_STATE
, which calls clearLocalWallet()
dispatch({ type: WalletActions.OPEN_KEEPKEY_DISCONNECT }) | ||
} else { | ||
history.replace(INITIAL_WALLET_MODAL_ROUTE) | ||
if (disconnectOnCloseModal) { | ||
disconnect() | ||
dispatch({ type: WalletActions.RESET_STATE }) | ||
store.dispatch(localWalletSlice.actions.clearLocalWallet()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
disconnect()
calls RESET_STATE
, which calls clearLocalWallet()
@@ -413,7 +433,6 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX | |||
*/ | |||
state.wallet?.disconnect?.() | |||
dispatch({ type: WalletActions.RESET_STATE }) | |||
store.dispatch(localWalletSlice.actions.clearLocalWallet()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
disconnect()
calls RESET_STATE
, which calls clearLocalWallet()
@@ -21,8 +21,7 @@ export const useWalletConnectV2EventHandler = ( | |||
*/ | |||
state.wallet?.disconnect?.() | |||
dispatch({ type: WalletActions.RESET_STATE }) | |||
localWallet.clearLocalWallet() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
disconnect()
calls RESET_STATE
, which calls clearLocalWallet()
dispatch({ | ||
type: WalletActions.SET_NATIVE_PENDING_DEVICE_ID, | ||
payload: deviceId, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Step 1: SET_NATIVE_PENDING_DEVICE_ID
@@ -83,6 +84,7 @@ export const EnterPassword = () => { | |||
type: WalletActions.SET_IS_CONNECTED, | |||
payload: true, | |||
}) | |||
dispatch({ type: WalletActions.RESET_NATIVE_PENDING_DEVICE_ID }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Step 2: Open the vault (line 64)
Step 3: RESET_NATIVE_PENDING_DEVICE_ID
@@ -103,6 +110,7 @@ export const NativeLoad = ({ history }: RouteComponentProps) => { | |||
type: WalletActions.SET_IS_CONNECTED, | |||
payload: true, | |||
}) | |||
dispatch({ type: WalletActions.RESET_NATIVE_PENDING_DEVICE_ID }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Step 2 (alternate): Vault already open, RESET_NATIVE_PENDING_DEVICE_ID
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revert me
if (!walletDeviceId) { | ||
console.error('Missing walletDeviceId') | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: early return before isDemoWallet
guard, since demo also has a deviceId
:
// For the demo wallet, we use the name, DemoWallet, as the deviceId |
@@ -66,7 +66,7 @@ export const useReceiveAddress = ({ | |||
? skipToken | |||
: async () => { | |||
// Already partially covered in isInitializing, but TypeScript lyfe mang. | |||
if (!buyAsset || !wallet || !buyAccountId || !buyAccountMetadata) { | |||
if (!buyAsset || !wallet || !buyAccountId || !buyAccountMetadata || !deviceId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not introduced by this diff, but prefer this guy (possibly for a follow-up, given this will change behaviour at runtime):
queryFn:
!isInitializing && buyAsset && wallet && buyAccountId && buyAccountMetadata && deviceId ? inlineQueryFn : skipToken
Rationale: all those checks should mostly be for the sake of type-safety, and we should not end up here, so realistically, should not make a difference, but we should always use skipToken
for type guards vs. early returns.
With the current version, there is a theoreticaly world where we have query runs where we return undefined, vs. not running the query altogether.
In this specific case, I don't think there should be any runtime issue with the current way (isLoading
going from true to false should be so fast it should be unnoticable), mostly for consistency.
@@ -38,7 +38,7 @@ export const KeepKeyPin = ({ | |||
}, | |||
dispatch, | |||
} = useWallet() | |||
const wallet = keyring.get(deviceId) | |||
const wallet = keyring.get(deviceId ?? '') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two qs: absolutely fine for the time being but
- do we want to pen a PR in hdwallet to make this guy a maybe instead of an optional to better reflect runtime types? https://github.com/shapeshift/hdwallet/blob/bd15119aad5a3b0410f4783dae946e146e960cf1/packages/hdwallet-core/src/keyring.ts#L50
diff --git a/packages/hdwallet-core/src/keyring.ts b/packages/hdwallet-core/src/keyring.ts
index 4701eb65..e8e43b54 100644
--- a/packages/hdwallet-core/src/keyring.ts
+++ b/packages/hdwallet-core/src/keyring.ts
@@ -47,7 +47,7 @@ export class Keyring extends eventemitter2.EventEmitter2 {
);
}
- public get<T extends HDWallet>(deviceID?: string): T | null {
+ public get<T extends HDWallet>(deviceID: string | null | undefined): T | null {
if (deviceID && this.aliases[deviceID] && this.wallets[this.aliases[deviceID]])
return this.wallets[this.aliases[deviceID]] as T;
if (deviceID && this.wallets[deviceID]) return this.wallets[deviceID] as T;
- do we want to
useMemo
this guy and early return, which will make it work both with current types and updated ones (if we decide to go with that) without nullish coalescing?
const wallet = keyring.get(deviceId ?? '') | |
const wallet = useMemo(() => { | |
if (!deviceId) return | |
return keyring.get(deviceId) | |
}, [deviceId, keyring]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also as far as I can see with LSP this is the only occurence of a maybe deviceId
, so only this specific occurence should be concerned by this comment
keyring.on(['KeepKey', deviceId ?? '', String(MessageType.FAILURE)], handleError) | ||
|
||
return () => { | ||
keyring.off(['KeepKey', deviceId, String(MessageType.FAILURE)], handleError) | ||
keyring.off(['KeepKey', deviceId ?? '', String(MessageType.FAILURE)], handleError) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: is setting event listeners without a deviceId
(theoretically runtime-wise, at least) intended? Since this effect is reactive on deviceId
, we should be able to early return, and to not return an unmount callback for the effect if (!deviceId
)`
@@ -52,7 +52,8 @@ export const LedgerChains = () => { | |||
|
|||
const handleConnectClick = useCallback( | |||
async (chainId: ChainId) => { | |||
if (!walletState?.wallet) { | |||
const { wallet, deviceId } = walletState ?? {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -58,6 +58,7 @@ export const EnterPassword = () => { | |||
const onSubmit = useCallback( | |||
async (values: FieldValues) => { | |||
try { | |||
if (!deviceId) return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💜
@@ -81,6 +81,13 @@ export const NativeLoad = ({ history }: RouteComponentProps) => { | |||
if (adapter) { | |||
const { name, icon } = NativeConfig | |||
try { | |||
// Set a pending device ID so the event handler doesn't redirect the user to password input |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧠
case NativeEvents.MNEMONIC_REQUIRED: | ||
if (!deviceId) break | ||
|
||
// Don't show password input for previous wallet when switching |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧠
// If we're on the native mobile app we don't need to handle the MNEMONIC_REQUIRED event as we use the device's native authentication instead | ||
// Reacting to this event will incorrectly open the native password modal after authentication completes when on the mobile app | ||
if (isMobileApp) break | ||
dispatch({ type: WalletActions.NATIVE_PASSWORD_OPEN, payload: { modal: true, deviceId } }) | ||
dispatch({ type: WalletActions.NATIVE_PASSWORD_OPEN, payload: { modal: true } }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💜
@@ -140,14 +142,15 @@ const reducer = (state: InitialState, action: ActionTypes): InitialState => { | |||
if (currentConnectedType === 'walletconnectv2') { | |||
state.wallet?.disconnect?.() | |||
store.dispatch(localWalletSlice.actions.clearLocalWallet()) | |||
store.dispatch(portfolioSlice.actions.setWalletMeta(undefined)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧠
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diff-wise looks excellent.
A few comments mostly re: types and cleanup, but shouldn't really change anything at runtime (the KK event listeners would just be EventEmitter spew rather than an actual bug, assuming we ever end up there).
Runtime pass incoming, stay tuned 🎸
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested localy, purposedly starting from a non nuked cache with these guys cached
- two native wallet
- two MIPD wallets
- Ledger
- KK
Wallet connection flow - right accounts derived and no race shenanigns
Native ✅
- Native is happy e2e: unlock, refresh and unlock, disconnect/reconnect, keystore import ✅
https://jam.dev/c/3a4e5f08-8f0b-4e04-943b-f04beb3f2974
Ledger ✅
https://jam.dev/c/9e00c010-d8ca-4738-bce1-a04e3a54d8ad
KeepKey ✅
https://jam.dev/c/c2825d4c-12dc-4e47-8720-1c09a1b8c728
MIPD Wallets ✅
https://jam.dev/c/3db330fc-71b8-4d09-af17-ae2089608d83
Phantom ✅
https://jam.dev/c/0bb2142a-7fab-4d48-83c5-34797c68ae2b
Signing
Native ✅
https://jam.dev/c/c29ed56d-1df7-44fc-98ff-7bad41b18294
MIPD ✅
https://jam.dev/c/c951c98e-ab0b-4326-8a38-ea420f7c787e
KK ✅
https://jam.dev/c/c7be8103-d524-406b-adb8-a90856481e30
Ledger ✅
https://jam.dev/c/89b5c194-f71b-49a6-ad3f-e149ba213150
Noted one smolish bug however, which I can also repro in develop, not introduced by this diff. Captured in #8535.
-
When connecting a mipd wallet with a given ENS associated to it, then connecting to Ledger (to another EVM address set, without ENS associated to it), stale ENS is displayed
-
this diff: https://jam.dev/c/d9a18722-74cb-41bc-8f38-aa8daa917d7f
-
develop: https://jam.dev/c/4635c6ea-dc26-4222-bec6-8c10f62345e1
Description
Patch for state corruption in wallet provider when user backs out of switching wallet. See #7904 for context.
The fix here is a patch to prevent corrupt state by preventing "back-out" of native connect into corrupt state by forcing the previous wallet to first disconnect before the password prompt is displayed.
Previous issue in simplified terms:
deviceId
with the new wallet device IDdeviceId
until they reconnect the walletThis PR:
deviceId
and the redux wallet ID are cleared, wallet is disconnectedSee below jam exaplaining in more detail.
Issue (if applicable)
closes #7904
Risk
High risk because it modifies WalletProvider and could theoretically result in inability to open a wallet.
All wallets, especially native.
Testing
Thoroughly test opening/closing/switching wallets, with more emphasis on native wallet.
Sanity check signin, broadcasting, trading etc is not somehow unexpectedly broken.
Engineering
Operations
Screenshots (if applicable)
Have a watch of my home movie explaining everything (audio on)
https://jam.dev/c/22a2572a-aeb6-4a82-a6c2-bd79bac3079d