Skip to content

Commit e35a9ed

Browse files
agangladagambinish
authored andcommitted
fix: resolve merge conflicts
1 parent 3e598ba commit e35a9ed

7 files changed

Lines changed: 115 additions & 70 deletions

app/controllers/perps/providers/HyperLiquidProvider.test.ts

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5963,7 +5963,7 @@ describe('HyperLiquidProvider', () => {
59635963
const result = await provider.placeOrder(orderParams);
59645964

59655965
// PR #25334: Builder fee approval is now non-blocking (fire-and-forget)
5966-
// to prevent QR popup spam for hardware wallets.
5966+
// to prevent repeated signing prompts for hardware wallets.
59675967
// Order should proceed even if builder fee approval fails.
59685968
expect(result.success).toBe(true);
59695969
expect(result.orderId).toBeDefined();
@@ -8957,12 +8957,12 @@ describe('HyperLiquidProvider', () => {
89578957
});
89588958

89598959
// ─────────────────────────────────────────────────
8960-
// dexAbstraction → unifiedAccount migration on init
8960+
// Signing-backed unifiedAccount migration on init
89618961
//
8962-
// The transition requires an EIP-712 prompt (HL blocks the agent path),
8963-
// so software-wallet users migrate during initial setup to ensure the
8964-
// first trade sees unified collateral. Hardware wallets remain deferred
8965-
// to avoid QR / Ledger prompt spam while browsing.
8962+
// Some transitions require an EIP-712 prompt, so software-wallet users
8963+
// migrate during initial setup to ensure the first trade sees unified
8964+
// collateral. Hardware wallets remain deferred to avoid repeated signing
8965+
// prompts while browsing.
89668966
// ─────────────────────────────────────────────────
89678967

89688968
it('calls userSetAbstraction on init for software-wallet dexAbstraction users', async () => {
@@ -9100,33 +9100,36 @@ describe('HyperLiquidProvider', () => {
91009100
).toHaveBeenCalledWith(USER_ADDRESS, 'unifiedAccount');
91019101
});
91029102

9103-
it('defers dexAbstraction migration on init for hardware wallets', async () => {
9104-
// Arrange
9105-
mockWalletService.isSelectedHardwareWallet.mockReturnValue(true);
9106-
const mockExchangeClient = createMockExchangeClient();
9107-
mockClientService.getInfoClient = jest.fn().mockReturnValue(
9108-
createMockInfoClient({
9109-
userAbstraction: jest.fn().mockResolvedValue('dexAbstraction'),
9110-
}),
9111-
);
9112-
mockClientService.getExchangeClient = jest
9113-
.fn()
9114-
.mockReturnValue(mockExchangeClient);
9103+
it.each(['dexAbstraction', 'default', 'disabled'] as const)(
9104+
'defers %s migration on init for hardware wallets',
9105+
async (currentMode) => {
9106+
// Arrange
9107+
mockWalletService.isSelectedHardwareWallet.mockReturnValue(true);
9108+
const mockExchangeClient = createMockExchangeClient();
9109+
mockClientService.getInfoClient = jest.fn().mockReturnValue(
9110+
createMockInfoClient({
9111+
userAbstraction: jest.fn().mockResolvedValue(currentMode),
9112+
}),
9113+
);
9114+
mockClientService.getExchangeClient = jest
9115+
.fn()
9116+
.mockReturnValue(mockExchangeClient);
91159117

9116-
// Act - init path
9117-
await provider.getMarketDataWithPrices();
9118+
// Act - init path
9119+
await provider.getMarketDataWithPrices();
91189120

9119-
// Assert - no browsing-time hardware prompt; action-time setup can still run.
9120-
expect(mockExchangeClient.userSetAbstraction).not.toHaveBeenCalled();
9121-
expect(mockExchangeClient.agentSetAbstraction).not.toHaveBeenCalled();
9122-
expect(
9123-
(TradingReadinessCache as jest.Mocked<typeof TradingReadinessCache>)
9124-
.set,
9125-
).not.toHaveBeenCalled();
9126-
expect(
9127-
mockSubscriptionService.setUserAbstractionMode,
9128-
).not.toHaveBeenCalled();
9129-
});
9121+
// Assert - no browsing-time hardware prompt; action-time setup can still run.
9122+
expect(mockExchangeClient.userSetAbstraction).not.toHaveBeenCalled();
9123+
expect(mockExchangeClient.agentSetAbstraction).not.toHaveBeenCalled();
9124+
expect(
9125+
(TradingReadinessCache as jest.Mocked<typeof TradingReadinessCache>)
9126+
.set,
9127+
).not.toHaveBeenCalled();
9128+
expect(
9129+
mockSubscriptionService.setUserAbstractionMode,
9130+
).not.toHaveBeenCalled();
9131+
},
9132+
);
91309133

91319134
it('does NOT call setUserAbstractionMode when migration fails', async () => {
91329135
const mockExchangeClient = createMockExchangeClient();

app/controllers/perps/providers/HyperLiquidProvider.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ import {
128128
addSpotBalanceToAccountState,
129129
aggregateAccountStates,
130130
} from '../utils/accountUtils';
131-
import { ensureError } from '../utils/errorUtils';
131+
import { ensureError, isKeyringLockedError } from '../utils/errorUtils';
132+
import { shouldDeferUnifiedAccountSetup } from '../utils/hyperLiquidAbstraction';
132133
import {
133134
adaptAccountStateFromSDK,
134135
adaptHyperLiquidLedgerUpdateToUserHistoryItem,
@@ -627,7 +628,8 @@ export class HyperLiquidProvider implements PerpsProvider {
627628
const network = this.#clientService.isTestnetMode() ? 'testnet' : 'mainnet';
628629

629630
// Check global cache first to avoid repeated signing requests
630-
// This is CRITICAL for hardware wallets to prevent QR popup spam
631+
// This is CRITICAL for hardware wallets to prevent repeated signing prompts
632+
// while browsing.
631633
const cachedStatus = TradingReadinessCache.get(network, userAddress);
632634
if (cachedStatus?.attempted) {
633635
this.#deps.debugLogger.log(
@@ -726,14 +728,14 @@ export class HyperLiquidProvider implements PerpsProvider {
726728
return;
727729
}
728730

729-
// Defer the user-signed transition until the user attempts an action.
731+
// Defer signing-backed transitions until the user attempts an action.
730732
// Cache is intentionally left untouched so the next entry re-evaluates;
731733
// the read-only userAbstraction call is cheap and gated by the in-flight
732734
// lock, preventing concurrent prompts.
733-
if (currentMode === 'dexAbstraction' && !allowUserSigning) {
735+
if (shouldDeferUnifiedAccountSetup(currentMode, allowUserSigning)) {
734736
this.#deps.debugLogger.log(
735-
'HyperLiquidProvider: Deferring dexAbstraction → unifiedAccount migration to action time',
736-
{ user: userAddress, network },
737+
'HyperLiquidProvider: Deferring unified account migration to action time',
738+
{ user: userAddress, network, mode: currentMode },
737739
);
738740
completeInFlight();
739741
return;
@@ -927,10 +929,10 @@ export class HyperLiquidProvider implements PerpsProvider {
927929
}
928930

929931
// Attempt Unified Account migration as early as possible so users aren't
930-
// blocked when they try to trade. Software-wallet dexAbstraction users can
931-
// complete the one-time EIP-712 migration during initial setup so the first
932-
// trade sees the unified balance. Hardware wallets remain deferred to
933-
// action time to avoid QR / Ledger prompt spam while browsing.
932+
// blocked when they try to trade. Software wallets can complete the
933+
// signing-backed migration during initial setup so the first trade sees
934+
// the unified balance. Hardware wallets remain deferred to action time to
935+
// avoid repeated signing prompts while browsing.
934936
await this.#ensureUnifiedAccountEnabled({
935937
allowUserSigning: !this.#walletService.isSelectedHardwareWallet(),
936938
});
@@ -963,7 +965,7 @@ export class HyperLiquidProvider implements PerpsProvider {
963965
* - Builder fee approval (required for orders)
964966
* - Referral code setup (attribution)
965967
*
966-
* These operations are DEFERRED from ensureReady() to avoid QR popup spam
968+
* These operations are DEFERRED from ensureReady() to avoid hardware wallet prompt spam
967969
* when users are just viewing the Perps section (critical for hardware wallets).
968970
*
969971
* Call this method before any trading operation (placeOrder, cancelOrder, etc.)
@@ -2542,11 +2544,12 @@ export class HyperLiquidProvider implements PerpsProvider {
25422544
const cacheKey = this.#getCacheKey(network, userAddress);
25432545

25442546
// Check GLOBAL cache first to avoid repeated signing requests across reconnections
2545-
// This is CRITICAL for hardware wallets to prevent QR popup spam
2547+
// This is CRITICAL for hardware wallets to prevent repeated signing prompts
2548+
// while browsing.
25462549
const globalCached = PerpsSigningCache.getBuilderFee(network, userAddress);
25472550
if (globalCached?.attempted) {
25482551
this.#deps.debugLogger.log(
2549-
'[ensureBuilderFeeApproval] Using global cache (prevents QR popup spam)',
2552+
'[ensureBuilderFeeApproval] Using global cache (prevents hardware wallet prompt spam)',
25502553
{ network, success: globalCached.success },
25512554
);
25522555
if (globalCached.success) {
@@ -8377,7 +8380,7 @@ export class HyperLiquidProvider implements PerpsProvider {
83778380
const globalCached = PerpsSigningCache.getReferral(network, userAddress);
83788381
if (globalCached?.attempted) {
83798382
this.#deps.debugLogger.log(
8380-
'[ensureReferralSet] Using global cache (prevents QR popup spam)',
8383+
'[ensureReferralSet] Using global cache (prevents hardware wallet prompt spam)',
83818384
{ network, success: globalCached.success },
83828385
);
83838386
return;

app/controllers/perps/services/HyperLiquidWalletService.test.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,13 @@ describe('HyperLiquidWalletService', () => {
322322
expect(service.isSelectedHardwareWallet()).toBe(false);
323323
});
324324

325-
it('returns true for Ledger hardware wallet', () => {
325+
it.each([
326+
'Ledger Hardware',
327+
'Trezor Hardware',
328+
'OneKey Hardware',
329+
'Lattice Hardware',
330+
'QR Hardware Wallet Device',
331+
])('returns true for %s wallet', (keyringType) => {
326332
(mockMessenger.call as jest.Mock).mockImplementation((action: string) => {
327333
if (
328334
action === 'AccountTreeController:getAccountsFromSelectedAccountGroup'
@@ -332,28 +338,7 @@ describe('HyperLiquidWalletService', () => {
332338
...mockEvmAccount,
333339
metadata: {
334340
...mockEvmAccount.metadata,
335-
keyring: { type: 'Ledger Hardware' },
336-
},
337-
},
338-
];
339-
}
340-
return undefined;
341-
});
342-
343-
expect(service.isSelectedHardwareWallet()).toBe(true);
344-
});
345-
346-
it('returns true for QR hardware wallet', () => {
347-
(mockMessenger.call as jest.Mock).mockImplementation((action: string) => {
348-
if (
349-
action === 'AccountTreeController:getAccountsFromSelectedAccountGroup'
350-
) {
351-
return [
352-
{
353-
...mockEvmAccount,
354-
metadata: {
355-
...mockEvmAccount.metadata,
356-
keyring: { type: 'QR Hardware Wallet Device' },
341+
keyring: { type: keyringType },
357342
},
358343
},
359344
];

app/controllers/perps/services/HyperLiquidWalletService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { findEvmAccount, getSelectedEvmAccount } from '../utils/accountUtils';
1818
// service portable between mobile and the core monorepo.
1919
const HARDWARE_KEYRING_TYPES = new Set<string>([
2020
'Ledger Hardware',
21+
'Trezor Hardware',
22+
'OneKey Hardware',
23+
'Lattice Hardware',
2124
'QR Hardware Wallet Device',
2225
]);
2326

@@ -55,7 +58,7 @@ export class HyperLiquidWalletService {
5558
/**
5659
* Check whether the selected EVM account is backed by hardware.
5760
*
58-
* @returns True for Ledger / QR hardware keyrings; false for software accounts.
61+
* @returns True for MetaMask hardware keyrings; false for software accounts.
5962
*/
6063
public isSelectedHardwareWallet(): boolean {
6164
const selectedEvmAccount = findEvmAccount(

app/controllers/perps/services/TradingReadinessCache.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* Global singleton cache for Perps signing operations
33
*
44
* This cache persists across provider reconnections to prevent repeated
5-
* signing requests for hardware wallets. Critical for preventing QR popup spam.
5+
* signing requests for hardware wallets. Critical for preventing repeated
6+
* hardware wallet signing prompts.
67
*
78
* Cache is intentionally kept separate from provider instances because providers
89
* are recreated on account/network changes, which would reset instance-level caches.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { shouldDeferUnifiedAccountSetup } from './hyperLiquidAbstraction';
2+
3+
describe('shouldDeferUnifiedAccountSetup', () => {
4+
it.each(['dexAbstraction', 'default', 'disabled'] as const)(
5+
'defers %s setup when signing is not allowed',
6+
(currentMode) => {
7+
expect(shouldDeferUnifiedAccountSetup(currentMode, false)).toBe(true);
8+
},
9+
);
10+
11+
it.each(['dexAbstraction', 'default', 'disabled'] as const)(
12+
'allows %s setup when signing is allowed',
13+
(currentMode) => {
14+
expect(shouldDeferUnifiedAccountSetup(currentMode, true)).toBe(false);
15+
},
16+
);
17+
18+
it.each(['unifiedAccount', 'portfolioMargin', undefined] as const)(
19+
'does not defer %s because no migration is required',
20+
(currentMode) => {
21+
expect(shouldDeferUnifiedAccountSetup(currentMode, false)).toBe(false);
22+
},
23+
);
24+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { HyperLiquidAbstractionMode } from '../types/hyperliquid-types';
2+
3+
const MIGRATABLE_ABSTRACTION_MODES = new Set<HyperLiquidAbstractionMode>([
4+
'dexAbstraction',
5+
'default',
6+
'disabled',
7+
]);
8+
9+
/**
10+
* Determine whether unified-account setup should be deferred until a user
11+
* explicitly starts a trading or withdrawal action.
12+
*
13+
* @param currentMode - The user's current HyperLiquid abstraction mode.
14+
* @param allowUserSigning - Whether the caller is allowed to trigger wallet signing.
15+
* @returns True when migration would require a signing-backed mutation that should be deferred.
16+
*/
17+
export function shouldDeferUnifiedAccountSetup(
18+
currentMode: HyperLiquidAbstractionMode | undefined,
19+
allowUserSigning: boolean,
20+
): boolean {
21+
return (
22+
!allowUserSigning &&
23+
currentMode !== undefined &&
24+
MIGRATABLE_ABSTRACTION_MODES.has(currentMode)
25+
);
26+
}

0 commit comments

Comments
 (0)