Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a370f2d
feat(perps-controller): add accountSigner platform dependency
abretonc7s Sep 28, 2026
98232e9
feat(perps-controller)!: remove headless Lighter signer options
abretonc7s Sep 28, 2026
36d86a7
fix(perps-controller): tighten accountSigner contract and coverage
abretonc7s Sep 28, 2026
4da0d41
feat(perps-controller): sign HyperLiquid L1 actions with a host-owned…
abretonc7s Sep 28, 2026
50554e1
fix(perps-controller): bind agents to their account and network
abretonc7s Sep 28, 2026
f62ce5b
fix(perps-controller): bind agents explicitly and harden resolution
abretonc7s Sep 28, 2026
5f3e2c4
fix(perps-controller): add agent unbinding and harden readiness
abretonc7s Sep 28, 2026
4859114
fix(perps-controller): close agent races and readiness gaps
abretonc7s Sep 28, 2026
dc9e909
docs(perps-controller): link changelog entries to #10559
abretonc7s Sep 28, 2026
b4944ff
test(perps-controller): load the HyperLiquid SDK only where Jest can …
abretonc7s Sep 28, 2026
4cf8b4c
fix(perps-controller): keep agent bindings on the controller and quie…
abretonc7s Sep 29, 2026
4f91876
fix(perps-controller): recover from rejected agents and keep signer f…
abretonc7s Sep 29, 2026
487015b
test(perps-controller): run the account-signer provider tests on the …
abretonc7s Sep 29, 2026
dc36612
fix(perps-controller): scope rejected-agent eviction and quiet retrya…
abretonc7s Sep 29, 2026
5b86138
fix(perps-controller): classify signer failures on every HyperLiquid …
abretonc7s Sep 29, 2026
8a4dd45
fix(perps-controller): classify signer failures on strategy cancels a…
abretonc7s Sep 29, 2026
4377138
fix(perps-controller): keep every signer failure retryable, and test …
abretonc7s Sep 29, 2026
e36665b
test(perps-controller): pin exact signer, write and result assertions
abretonc7s Sep 29, 2026
563bf12
fix(perps-controller): read signer failures from cancel status entrie…
abretonc7s Sep 29, 2026
4e8abc6
fix(perps-controller): report a rejected agent once, with the host's …
abretonc7s Sep 29, 2026
413991c
fix(perps-controller): keep a signer that locks mid-signature retryab…
abretonc7s Sep 29, 2026
9c19c3f
fix(perps-controller): skip unusable builder fee prompts and report l…
abretonc7s Sep 29, 2026
b8fc59f
fix(perps-controller): retry the referral after a first deposit, and …
abretonc7s Sep 29, 2026
eb71122
fix(perps-controller): report an unfunded wallet in preparation, and …
abretonc7s Sep 29, 2026
b18368a
fix(perps-controller): check a pending referral code only in preparat…
abretonc7s Sep 29, 2026
4461ea2
fix(perps-controller): keep cancelled TP/SL legs next to a rejected a…
abretonc7s Sep 29, 2026
0b9da51
fix(perps-controller): read thrown cancel statuses per entry, restore…
abretonc7s Sep 29, 2026
eb12dd7
fix(perps-controller): report a preparation whose account changed as …
abretonc7s Sep 29, 2026
f18b9ff
fix(perps-controller): check the prepared account before any result, …
abretonc7s Sep 29, 2026
4595921
docs(perps-controller): say when the setAgentSigner messenger action …
abretonc7s Sep 29, 2026
110be95
docs(perps-controller): explain Lighter API key slots and the differe…
abretonc7s Sep 29, 2026
795d09b
test(perps-controller): check the signer surface through the package …
abretonc7s Sep 29, 2026
3e028d4
test(perps-controller): mock the SDK in the public API test so it run…
abretonc7s Sep 29, 2026
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
8 changes: 0 additions & 8 deletions oxlint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4494,14 +4494,6 @@
"count": 3
}
},
"packages/perps-controller/tests/src/services/LighterWalletService.test.ts": {
"typescript/no-unsafe-assignment": {
"count": 1
},
"typescript/unbound-method": {
"count": 1
}
},
"packages/perps-controller/tests/src/services/TerminalMarketService.test.ts": {
"no-unsafe-optional-chaining": {
"count": 1
Expand Down
36 changes: 36 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Persist the Isolated/Cross margin-mode pick per market and network in `tradeConfigurations[network][symbol].marginMode`, so clients can restore it after the order form remounts and share it across Mobile and Extension ([#10464](https://github.com/MetaMask/core/pull/10464))
- Add `getMarginMode(symbol)` and `saveMarginMode(symbol, marginMode)` methods, exposed as the `PerpsController:getMarginMode` and `PerpsController:saveMarginMode` messenger actions (`PerpsControllerGetMarginModeAction`, `PerpsControllerSaveMarginModeAction`). `saveMarginMode` ignores values other than `isolated` or `cross`.
- Add the `selectMarginMode(state, symbol)` selector and an optional `marginMode` field on `TradeConfiguration`.
- Add optional `accountSigner` to `PerpsPlatformDependencies` so clients without a `KeyringController` can sign through their own wallet ([#10559](https://github.com/MetaMask/core/pull/10559))
- Export the new `PerpsAccountSigner` and `PerpsTypedDataPayload` types
- When set, HyperLiquid typed-data signing and Lighter `personal_sign` go through it and never call the `KeyringController:*` messenger actions; the signing address still comes from the messenger's selected account
- `isReady()` returning `false` fails signing with the existing `KEYRING_LOCKED` error code
- `requiresSignatureConfirmation()` defers HyperLiquid's optional init-time signing prompts like a hardware keyring does; when omitted, the selected account's keyring type decides
- Add HyperLiquid agent signing so orders, cancels and other L1 actions are signed by a host-owned agent key instead of prompting the main wallet ([#10559](https://github.com/MetaMask/core/pull/10559))
- Add optional `providerCredentials.hyperliquid.getAgentSigner(account)`, which resolves the approved agent (new exported `PerpsAgentSigner` and `PerpsAgentAccount` types) when an L1 action is signed for that main account and network, including the unified-account migration the provider may sign while connecting
- An agent `getAgentSigner` returns is kept for the provider's lifetime or until `setAgentSigner`/`clearAgentSigners`; `null` and failures are asked again at the next L1 action
- An agent whose signing throws fails that action with `KEYRING_LOCKED` and stays in use, so a host calls `clearAgentSigners` when its agent key locks
- Add `PerpsController:setAgentSigner(account, agentSigner)` (`PerpsControllerSetAgentSignerAction`) to bind an agent to an explicit main account and network, or pin that account to the main wallet with `null`; the controller keeps bindings across provider re-creation; `setAgentSigner()` can be called on the controller before `init`, and the messenger action is available once `init` has run
- Add `PerpsController:clearAgentSigners` (`PerpsControllerClearAgentSignersAction`) to forget every agent, for example when the wallet locks, so the next L1 action asks `getAgentSigner` again
- Add optional `PerpsProvider.clearAgentSigners`, implemented by the HyperLiquid provider
- An agent the venue rejects as unknown (revoked or expired, for example after the user approves another unnamed agent) is dropped, together with a `setAgentSigner` binding to it, so the next L1 action asks `getAgentSigner` again; the rejected action fails with `KEYRING_LOCKED` instead of `EXCHANGE_ACCOUNT_NOT_FOUND`
- Add optional `providerCredentials.hyperliquid.onAgentRejected(account, agentAddress)`, called with the agent's address as the client supplied it for each write the venue rejects with that agent, so the client can re-check its approval
- The exported `HyperLiquidProvider` accepts the matching optional `getAgentSigner` and `onAgentRejected` constructor options and implements `clearAgentSigners`
- An agent only ever signs for the main account and network it was set or resolved for, and user-signed actions (builder fee, withdraw, the user-signed migration from `dexAbstraction`, ...) always stay on the main account; approving the agent remains the client's job
- Export `HYPERLIQUID_L1_ACTION_PRIMARY_TYPE` and `HYPERLIQUID_L1_ACTION_DOMAIN_NAME`, the EIP-712 shape that marks an L1 action
- Add `PerpsController:prepareTradingWallet` (`PerpsControllerPrepareTradingWalletAction`) and optional `PerpsProvider.prepareTradingWallet` to run the deferred trading setup before the first order, so its signatures happen in a guided session: account migration, builder fee and referral on HyperLiquid, venue-key registration on Lighter ([#10559](https://github.com/MetaMask/core/pull/10559))
- The builder fee, the migration from `dexAbstraction` and the Lighter registration are signed by the main account; with an agent, the HyperLiquid referral and silent migration are signed by the agent
- Resolves a `ReadyToTradeResult` that is `ready: true` once an account is selected, the main-account signer is ready and none of these steps will need a signature again before the first order, and `ready: false` while one will be retried, including after an agent could not sign; `ready: false` carries `KEYRING_LOCKED` while the signer is not ready, `EXCHANGE_ACCOUNT_NOT_FOUND` for a wallet with no account on the venue yet, `NO_ACCOUNT_SELECTED`, `PROVIDER_LIFECYCLE_STALE` when the provider or account changed during setup, or the message of the logged error that stopped setup; the aggregated provider prepares every provider in turn
- A HyperLiquid referral whose MetaMask referral code is not ready yet does not hold the result back; the next `prepareTradingWallet` checks the code again, and orders do not
- Implemented by the exported `HyperLiquidProvider` and by the Lighter provider, which resolves `ready: true` at once when it is read-only (no signer bridge), an account is selected and the main-account signer is ready
- Add optional `isTestnet` to `AggregatedProviderConfig`, which tags the errors the aggregated provider logs with the network ([#10559](https://github.com/MetaMask/core/pull/10559))

### Removed

- **BREAKING:** Remove the `LighterPersonalSigner` type and the `personalSigner` and `l1Address` fields of `LighterAuthConfig` ([#10559](https://github.com/MetaMask/core/pull/10559))
- `PerpsController` never forwarded these fields to the Lighter provider, so they had no effect for controller clients
- To sign Lighter L1 messages without a `KeyringController`, set `PerpsPlatformDependencies.accountSigner.signPersonalMessage`; the L1 address comes from the messenger's selected account

### Fixed

- HyperLiquid writes that fail because the keyring is locked now fail with `KEYRING_LOCKED` and are no longer reported as errors by the provider or `TradingService` ([#10559](https://github.com/MetaMask/core/pull/10559))
- Before, they failed with the SDK's "Failed to sign the typed data using the wallet" message, or with `TPSL_UPDATE_FAILED` for a TP/SL update whose builder fee was not approved yet
- Covers orders, edits, single and batch cancels (TWAP, scale and chase cancels included), position closes, TP/SL updates and clears, margin updates, withdrawals and transfers between DEXs, including the HIP-3 transfers around an order
- HyperLiquid `cancelOrders` reports each order of a batch with its own result when an entry fails: orders the venue cancelled are no longer reported as failed with the batch's error ([#10559](https://github.com/MetaMask/core/pull/10559))

## [18.0.1]

Expand Down
41 changes: 41 additions & 0 deletions packages/perps-controller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,47 @@ an asset with an open position, resting order, or active native TWAP schedule,
including schedules whose first slice has not filled. Orders in the same mode may
increase or reduce the existing position.

## Signing without a `KeyringController`

By default the controller signs through the `KeyringController:*` messenger
actions. A client without a keyring passes `accountSigner` in its platform
dependencies (`signTypedData`, `signPersonalMessage`, optional `isReady` and
`requiresSignatureConfirmation`); the signing address still comes from the
selected account, and a signer that is not ready fails with `KEYRING_LOCKED`.

HyperLiquid L1 actions (orders, cancels, leverage, ...) can be signed by a
client-owned agent key: return it from
`providerCredentials.hyperliquid.getAgentSigner(account)`, or bind it to an
account and network with `PerpsController:setAgentSigner`. User-signed actions
(builder fee, withdrawals) stay on the main account, and approving the agent
is the client's job. When the venue rejects an agent (revoked or expired), the
write fails with `KEYRING_LOCKED`, the agent is dropped and
`providerCredentials.hyperliquid.onAgentRejected` is called. Call
`PerpsController:clearAgentSigners` when the agent key locks.

`PerpsController:prepareTradingWallet` runs the setup that needs signatures
(HyperLiquid account migration, builder fee and referral; Lighter key
registration) before the first order, so a hardware or external wallet signs
it in one guided session.

## Lighter trading keys

Lighter orders are not signed by the wallet. A Lighter account (owned by the
wallet's address) holds trading keys, called API keys, in numbered slots. The
client's signer bridge generates the key for the slot set in
`providerCredentials.lighter.apiKeyIndex` (default `7`) and keeps its private
half on the device. The wallet signs one `personal_sign` message to register it
in that slot, during `PerpsController:prepareTradingWallet` or before the first
order; after that, orders are signed with the key and need no wallet prompt.

A key only works where it was generated, so give each device or app instance
its own slot. When the slot already holds a key this signer did not create,
the provider stops with "Lighter API key slot N already contains a different
key" instead of replacing it, since that key may still be in use elsewhere. Use
a free slot instead: the Lighter API answers "api key not found" for
`GET /api/v1/apikeys?account_index=<account>&api_key_index=<slot>` when the
slot is free.

## Contributing

This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,75 @@ export type PerpsControllerCalculateFeesAction = {
handler: PerpsController['calculateFees'];
};

/**
* Sign HyperLiquid L1 actions (orders, cancels, leverage, ...) for a main
* account on a network with an approved agent, or pin them to the main
* account with null (`getAgentSigner` is then not asked for that account and
* network until `clearAgentSigners`). User-signed actions stay on the main
* account, and the agent is never used for another account or network. The
* controller keeps the binding across provider re-creation (a provider or
* network switch, or re-initialization), so it can also be set before
* `init`. Like every controller action, it is available through the
* messenger once `init` has run.
*
* @param account - The main account and network the agent is approved for.
* @param agentSigner - The host-owned agent signer, or null to pin the main
* account.
*/
export type PerpsControllerSetAgentSignerAction = {
type: `PerpsController:setAgentSigner`;
handler: PerpsController['setAgentSigner'];
};

/**
* Forget every HyperLiquid agent, set or resolved, so the next L1 action
* asks `providerCredentials.hyperliquid.getAgentSigner` again; an answer
* still pending is discarded too. Call it when the wallet locks (with
* `getAgentSigner` returning null while locked) and nothing signs with an
* agent until it returns one again. Like every controller action, it is
* available through the messenger once `init` has run.
*/
export type PerpsControllerClearAgentSignersAction = {
type: `PerpsController:clearAgentSigners`;
handler: PerpsController['clearAgentSigners'];
};

/**
* Run the active provider's deferred trading setup ahead of the first order
* (HyperLiquid account migration, builder fee and referral; Lighter
* venue-key registration), so its signatures happen in one guided session,
* such as agent setup, instead of at order time. The builder fee, the
* migration from `dexAbstraction` and Lighter's registration are signed by
* the main account; with an agent, the referral and the silent migration
* are L1 actions the agent signs.
*
* @returns `ready: true` when none of these steps will need a signature
* again before the first order, and only while an account is selected and
* the main account can sign, whichever provider answered (including
* providers without deferred setup, for example in aggregated mode). A
* declined HyperLiquid migration is not asked again, and a HyperLiquid
* referral whose MetaMask referral code is not ready yet is checked again at
* the next call, not before orders, so neither holds it back. Otherwise
* `ready: false`, without an error while a step will be asked again (a
* declined builder fee or Lighter registration, or a step the agent could
* not sign), or with:
* - `KEYRING_LOCKED` when the main account cannot sign, before or during
* setup;
* - `EXCHANGE_ACCOUNT_NOT_FOUND` for a wallet with no account on the venue
* yet;
* - `NO_ACCOUNT_SELECTED` when no account is selected;
* - `PROVIDER_LIFECYCLE_STALE` when the provider disconnected or the account
* changed during setup;
* - otherwise the message of the error that stopped setup, which is logged.
* @throws Like the other provider-backed actions, `CLIENT_NOT_INITIALIZED`
* before `init`, and `CLIENT_REINITIALIZING` or `PROVIDER_NOT_AVAILABLE`
* when no active provider is available.
*/
export type PerpsControllerPrepareTradingWalletAction = {
type: `PerpsController:prepareTradingWallet`;
handler: PerpsController['prepareTradingWallet'];
};

/**
* Approve the dedicated subscription builder outside order submission.
*
Expand Down Expand Up @@ -1453,6 +1522,9 @@ export type PerpsControllerMethodActions =
| PerpsControllerSubscribeToOICapsAction
| PerpsControllerSetLiveDataConfigAction
| PerpsControllerCalculateFeesAction
| PerpsControllerSetAgentSignerAction
| PerpsControllerClearAgentSignersAction
| PerpsControllerPrepareTradingWalletAction
| PerpsControllerApproveSubscriptionBuilderFeeAction
| PerpsControllerInvalidateSubscriptionBenefitsAction
| PerpsControllerDisconnectAction
Expand Down
Loading
Loading