Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add an agent-wallet signing seam for HyperLiquid: the optional `PerpsControllerOptions.getAgentSigner` callback resolves a local agent signer (exported `AgentSigner` type) whose key never touches the keyring, and `PerpsController:setTradingWalletOverride` switches (or clears) the HyperLiquid signing wallet at runtime. L1 actions are signed by the agent key; user-signed actions (`approveBuilderFee`, `sendAsset`, `withdraw3`, …) always fall back to the master wallet ([#10075](https://github.com/MetaMask/core/pull/10075))
- Add `PerpsController:prepareTradingWallet` and the optional `PerpsProvider.prepareTradingWallet` hook to run deferred trading-readiness steps (unified account enablement with user signing, builder fee approval) ahead of the first order, so required master signatures surface in a guided session rather than at order time ([#10075](https://github.com/MetaMask/core/pull/10075))

### Removed

- **BREAKING:** Remove all MYX protocol support ([#10038](https://github.com/MetaMask/core/pull/10038))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,16 @@ export type PerpsControllerClearAttributionContextAction = {
handler: PerpsController['clearAttributionContext'];
};

/**
* Override (or clear) the trading wallet the HyperLiquid provider signs with
* (agent signer on activation, `null` on keyring lock to restore the master
* path). See {@link PerpsController.setTradingWalletOverride}.
*/
export type PerpsControllerSetTradingWalletOverrideAction = {
type: `PerpsController:setTradingWalletOverride`;
handler: PerpsController['setTradingWalletOverride'];
};

/**
* Toggle between testnet and mainnet
*
Expand Down Expand Up @@ -917,6 +927,17 @@ export type PerpsControllerApproveSubscriptionBuilderFeeAction = {
handler: PerpsController['approveSubscriptionBuilderFee'];
};

/**
* Run the deferred trading-readiness steps (unified account enablement with
* user signing, builder fee approval) ahead of the first order, so any
* required master signature surfaces in a guided session rather than at
* order time. See {@link PerpsController.prepareTradingWallet}.
*/
export type PerpsControllerPrepareTradingWalletAction = {
type: `PerpsController:prepareTradingWallet`;
handler: PerpsController['prepareTradingWallet'];
};

/**
* Drop the cached subscription benefits snapshot.
*
Expand Down Expand Up @@ -1404,6 +1425,7 @@ export type PerpsControllerMethodActions =
| PerpsControllerSetAttributionContextAction
| PerpsControllerGetAttributionContextAction
| PerpsControllerClearAttributionContextAction
| PerpsControllerSetTradingWalletOverrideAction
| PerpsControllerToggleTestnetAction
| PerpsControllerSwitchProviderAction
| PerpsControllerGetCurrentNetworkAction
Expand All @@ -1423,6 +1445,7 @@ export type PerpsControllerMethodActions =
| PerpsControllerSetLiveDataConfigAction
| PerpsControllerCalculateFeesAction
| PerpsControllerApproveSubscriptionBuilderFeeAction
| PerpsControllerPrepareTradingWalletAction
| PerpsControllerInvalidateSubscriptionBenefitsAction
| PerpsControllerDisconnectAction
| PerpsControllerStartEligibilityMonitoringAction
Expand Down
61 changes: 61 additions & 0 deletions packages/perps-controller/src/PerpsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { DataLakeService } from './services/DataLakeService.js';
import { DepositService } from './services/DepositService.js';
import { EligibilityService } from './services/EligibilityService.js';
import { FeatureFlagConfigurationService } from './services/FeatureFlagConfigurationService.js';
import type { AgentSigner } from './services/HyperLiquidWalletService.js';
import { MarketDataService } from './services/MarketDataService.js';
import { RewardsIntegrationService } from './services/RewardsIntegrationService.js';
import type { ServiceContext } from './services/ServiceContext.js';
Expand Down Expand Up @@ -869,6 +870,15 @@ export type PerpsControllerOptions = {
* geolocation fetch from firing during wallet onboarding (privacy compliance).
*/
deferEligibilityCheck?: boolean;
/**
* Resolves the local agent signer for a master account address, or null
* when no agent is active or the wallet is locked. When provided, HyperLiquid
* signing switches to the agent key without touching the keyring; use
* `setTradingWalletOverride` to switch while running.
*/
getAgentSigner?: (
masterAccountAddress: string,
) => Promise<AgentSigner | null>;
};

type BlockedRegionList = {
Expand Down Expand Up @@ -955,6 +965,7 @@ const MESSENGER_EXPOSED_METHODS = [
'markTutorialCompleted',
'placeOrder',
'previewPositionModify',
'prepareTradingWallet',
'reconnect',
'recordMarketViewed',
'refreshEligibility',
Expand All @@ -975,6 +986,7 @@ const MESSENGER_EXPOSED_METHODS = [
'setLiveDataConfig',
'setSelectedPaymentToken',
'setVisibleCandleCount',
'setTradingWalletOverride',
'startEligibilityMonitoring',
'startMarketDataPreload',
'stopEligibilityMonitoring',
Expand Down Expand Up @@ -1150,6 +1162,12 @@ export class PerpsController extends BaseController<

#userDiskWrite: Promise<void> = Promise.resolve();

// Resolves the local agent signer for the selected master account, or null.
// Passed through to the HyperLiquid provider for the agent signing seam.
readonly #getAgentSigner:
| ((masterAccountAddress: string) => Promise<AgentSigner | null>)
| undefined = undefined;

// Store options for dependency injection (allows core package to inject platform-specific services)
readonly #options: PerpsControllerOptions;

Expand Down Expand Up @@ -1185,6 +1203,7 @@ export class PerpsController extends BaseController<
clientConfig = {},
infrastructure,
deferEligibilityCheck = false,
getAgentSigner,
}: PerpsControllerOptions) {
super({
name: 'PerpsController',
Expand All @@ -1194,6 +1213,7 @@ export class PerpsController extends BaseController<
});

this.#eligibilityCheckDeferred = deferEligibilityCheck;
this.#getAgentSigner = getAgentSigner;

// Store options for dependency injection
this.#options = {
Expand Down Expand Up @@ -1774,6 +1794,7 @@ export class PerpsController extends BaseController<
priceDeviationLimit: this.#priceDeviationLimit,
platformDependencies: this.#options.infrastructure,
messenger: this.messenger,
getAgentSigner: this.#getAgentSigner,
builderAddressTestnet:
this.#options.clientConfig?.providerCredentials?.hyperliquid
?.builderAddressTestnet,
Expand Down Expand Up @@ -2310,6 +2331,7 @@ export class PerpsController extends BaseController<
priceDeviationLimit: this.#priceDeviationLimit,
platformDependencies: this.#options.infrastructure,
messenger: this.messenger,
getAgentSigner: this.#getAgentSigner,
builderAddressTestnet:
this.#options.clientConfig?.providerCredentials?.hyperliquid
?.builderAddressTestnet,
Expand Down Expand Up @@ -5091,6 +5113,26 @@ export class PerpsController extends BaseController<
return { ...utm, ...properties };
}

/**
* Override (or clear) the trading wallet the HyperLiquid provider signs with.
*
* The host app calls this with a local agent signer when an agent wallet
* activates, and with `null` when the keyring locks (restoring the master
* keyring path). Throws when the HyperLiquid provider is not (yet)
* registered — callers should treat that as best-effort.
*
* @param signer - The agent signer to sign with, or null for the master path.
* @returns A promise that resolves when the clients have re-initialized with
* the new wallet.
*/
async setTradingWalletOverride(signer: AgentSigner | null): Promise<void> {
const provider = this.providers.get('hyperliquid');
if (!(provider instanceof HyperLiquidProvider)) {
throw new Error(PERPS_ERROR_CODES.PROVIDER_NOT_AVAILABLE);
}
return provider.setTradingWalletOverride(signer);
}

/**
* Toggle between testnet and mainnet
*
Expand Down Expand Up @@ -5798,6 +5840,25 @@ export class PerpsController extends BaseController<
: false;
}

/**
* Run the deferred trading-readiness steps for the active provider
* (unified account enablement with user signing, builder fee approval) so
* any required master signature surfaces during a guided session (e.g.
* agent wallet setup) instead of as a surprise prompt on the first order.
*
* Reuses the provider's own trading-readiness sequence, which is cached:
* an already-ready wallet completes without any signature. Providers
* without deferred setup make this a no-op. Best-effort by design of the
* underlying sequence: builder-fee failures are swallowed there and retried
* at order time; only initialization/migration errors propagate.
*/
async prepareTradingWallet(): Promise<void> {
const provider = await this.#getActiveProviderWhenReady();
if (provider.prepareTradingWallet) {
await provider.prepareTradingWallet();
}
}

/**
* Drop the cached subscription benefits snapshot.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/perps-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export type {
PerpsControllerMarkFirstOrderCompletedAction,
PerpsControllerMarkTutorialCompletedAction,
PerpsControllerPlaceOrderAction,
PerpsControllerPrepareTradingWalletAction,
PerpsControllerReconnectAction,
PerpsControllerRecordMarketViewedAction,
PerpsControllerRefreshEligibilityAction,
Expand Down Expand Up @@ -714,6 +715,7 @@ export {
// Services (only externally consumed items)
export { TradingReadinessCache } from './services/TradingReadinessCache.js';
export type { ServiceContext } from './services/ServiceContext.js';
export type { AgentSigner } from './services/HyperLiquidWalletService.js';
export {
AggregatedOrderBookConnection,
processAggregatedOrderBook,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,14 @@ export class AggregatedPerpsProvider implements PerpsProvider {
: false;
}

async prepareTradingWallet(): Promise<void> {
const provider =
this.#providers.get('hyperliquid') ?? this.#getDefaultProvider();
if (provider.prepareTradingWallet) {
await provider.prepareTradingWallet();
}
}

// ============================================================================
// Lifecycle (Delegate to default provider)
// ============================================================================
Expand Down
Loading
Loading