feat(perps-controller): sign without KeyringController and with a HyperLiquid agent - #10559
Conversation
Clients without a KeyringController can now sign through their own wallet. PerpsPlatformDependencies gains an optional accountSigner (signTypedData, signPersonalMessage, isReady, isHardwareWallet). When it is set, the HyperLiquid and Lighter wallet services sign through it instead of the KeyringController:* messenger actions. Clients that do not set it keep the current behaviour. isReady() returning false fails with the existing KEYRING_LOCKED code, and isHardwareWallet() drives the same prompt deferral as the keyring-type check.
BREAKING CHANGE: remove the LighterPersonalSigner type and the personalSigner and l1Address fields of LighterAuthConfig. PerpsController never forwarded these fields to the Lighter provider, so they had no effect for controller clients. accountSigner.signPersonalMessage replaces the injected signer, and the L1 address comes from the messenger's selected account. The Lighter e2e now signs through accountSigner with a selected-account messenger.
- Make signPersonalMessage required. When accountSigner is set, neither wallet service calls KeyringController, and isReady gates both. - Default isHardwareWallet to the selected account's keyring type instead of false. - Branch to accountSigner inside the Hyperliquid wallet adapter, where the payload is typed, instead of casting in the keyring path. - Cover real provider flows with a keyringless messenger (the Hyperliquid unified-account migration and the Lighter ChangePubKey registration), and cover the controller passing accountSigner to both providers.
… agent Adds agent (API wallet) signing on top of accountSigner. A host-owned PerpsAgentSigner, resolved through providerCredentials.hyperliquid .getAgentSigner or set at runtime with PerpsController:setAgentSigner, signs L1 actions (Agent type over the Exchange domain). User-signed actions stay on the main account, through accountSigner or the keyring. Switching the agent rebuilds only the SDK exchange client over the existing HTTP transport (HyperLiquidClientService.setWallet), so live WebSocket subscriptions keep running. PerpsController:prepareTradingWallet runs the deferred trading-readiness steps before the first order. Also addresses self-review round 2: document the EIP712Domain entry in PerpsTypedDataPayload, share the payload type with the SDK wallet params, log the Lighter signing address on both paths, strengthen the not-ready tests and scope the isHardwareWallet changelog note to HyperLiquid. Co-authored-by: Monte Lai <monte.lai@consensys.net>
Resolve the HyperLiquid agent when an L1 action is signed, keyed by the
selected main account and network, instead of swapping the SDK wallet.
An agent now never signs for another account or network, reads never call
getAgentSigner (whose failure only fails the L1 action that needed it), and
the exchange-client swap is no longer needed.
- getAgentSigner receives { mainAddress, isTestnet }.
- setAgentSigner binds to the selected account and network.
- prepareTradingWallet resolves a ReadyToTradeResult that is not ready
while a step still needs a signature.
- Verify agent routing through the SDK's own signing functions.
Co-authored-by: Monte Lai <monte.lai@consensys.net>
- setAgentSigner(account, agentSigner) takes the main account and network the agent is approved for, so a pending account switch or re-initialization cannot attach it to another account. getAgentSigner receives the same PerpsAgentAccount. - Keep only non-null getAgentSigner answers; null and failures are asked again at the next L1 action. A binding set while an answer is pending wins. - Hold the agent resolver in HyperLiquidWalletService so every wallet adapter it creates, including the subscription service's, routes L1 actions. - Log resolver failures once (the migration path reports them), move the L1 action constants to hyperLiquidConfig, and document what ready means. - Recover signers from real SDK signatures and cover the resolver races. Co-authored-by: Monte Lai <monte.lai@consensys.net>
- Add PerpsController:clearAgentSigners (and optional PerpsProvider.setAgentSigner / clearAgentSigners) so a host can stop agent signing on lock and let getAgentSigner answer again after unlock; setAgentSigner(account, null) pins the main account until then. - Treat a failed or synchronously throwing getAgentSigner as retryable (AgentSignerUnavailableError), so the referral write is retried instead of being recorded as failed for the session. - prepareTradingWallet reports KEYRING_LOCKED whenever the main-account signer is not ready and logs unexpected failures; Lighter prepares its venue-key registration and the aggregated provider prepares every provider. - Export the L1 action constants from the package root. Co-authored-by: Monte Lai <monte.lai@consensys.net>
- clearAgentSigners discards getAgentSigner answers still pending, and an answer superseded by a clear or a newer binding is resolved again, so a failure always surfaces as the retryable AgentSignerUnavailableError. - clearAgentSigners is a synchronous no-op without an initialized HyperLiquid provider, so a wallet-lock handler can always call it. - A referral write that failed on the agent signer keeps trading setup retryable, so prepareTradingWallet reports not ready and retries it. - Lighter prepareTradingWallet reports KEYRING_LOCKED whenever the main signer is not ready and logs failures; the aggregated provider isolates each provider's failure. Co-authored-by: Monte Lai <monte.lai@consensys.net>
…require it The SDK ships ES modules only, and Jest can require them only on Node 24.9 or newer, so the SDK signing suite failed to load in the Node 22 CI job. It now loads the SDK with jest.requireActual and runs on Node 24.9+; the other suites in the file still run on Node 22.
…t retryable failures - PerpsController keeps setAgentSigner bindings (AgentBindings) across HyperLiquid provider re-creation and before init; a changed binding drops the agents the provider already resolved. PerpsProvider.setAgentSigner is removed; the provider only caches getAgentSigner answers. - An agent whose signTypedData rejects is retryable like a failing getAgentSigner. Neither is logged as an error during the referral write or the silent unified-account migration. - Lighter prepareTradingWallet logs only unexpected failures, with the original error, and the aggregated provider logs a provider that throws. - Document the chain IDs HyperLiquid payloads carry for accountSigner and what isHardwareWallet means for interactive wallets.
…ailures retryable - An agent the venue rejects as unknown (revoked or expired) is evicted, with a setAgentSigner binding to it, so the next L1 action asks getAgentSigner again. The rejection no longer reads as a wallet with no HyperLiquid account in orders, the silent migration or the referral. - Orders and other exchange writes that fail because the keyring is locked or the agent is unavailable fail with KEYRING_LOCKED, and a failed order for that reason is not reported as an error. - A provider waiting on another provider's referral attempt makes its own when that attempt cached nothing. - Lighter prepareTradingWallet resolves not ready without an error when the user declines or has no Lighter account yet, and logs a missing signer bridge. The aggregated provider tags logged errors with the network. - Rename PerpsAccountSigner.isHardwareWallet to requiresSignatureConfirmation, which covers interactive wallets too. - Add a controller-level test that drives agent bindings through a real HyperLiquid provider and wallet service.
Carries MetaMask/core#10559 as a Yarn patch of @metamask/perps-controller 18.0.1 (built dist only: 18.0.1 plus that PR's source changes) until the release is published. Mobile sets none of the new hooks and keeps signing through KeyringController.
…real wallet service and caches Moves the account-signer block out of the account-mode suite, whose file-wide mocks replaced the wallet service and the signing caches. The new suite mocks only the client, subscription and SDK boundaries and clears the real caches before each test, so the idempotency and retry tests read the cache the provider writes.
…ble cancels - Match a venue rejection only against the agent of the selected account and network that signed the action, so an agent shared across accounts is evicted for that account alone. - Cancel and batch cancel no longer log a signer that could not sign (KEYRING_LOCKED) as an error. - Lighter prepareTradingWallet reports KEYRING_LOCKED when the signer locks while the venue key is being registered.
…write - Edits, position closes, TP/SL and margin updates evict a rejected agent and fail with KEYRING_LOCKED without being logged, like orders and cancels. A TP/SL update whose cancel the signer could not sign keeps the old protection instead of reporting it lost. TradingService no longer logs KEYRING_LOCKED results. - A rejection of an agent replaced while its action was in flight is still recognized, from every agent address the provider signed with. - Add providerCredentials.hyperliquid.onAgentRejected so the client can re-check its approval, and document what KEYRING_LOCKED covers. - HyperLiquid prepareTradingWallet returns KEYRING_LOCKED before running any setup while the signer is not ready, and the referral write no longer logs a locked keyring. - Rename HyperLiquidWalletService.isKeyringUnlocked to isMainAccountSignerReady, like the Lighter wallet service. - Tests: controller-level agent flows through the real provider, every write path, shared agent fixtures, and stronger readiness assertions.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5b86138. Configure here.
…nd TP/SL clears - Scale cancels report a signer failure from either batch (order IDs or client order IDs) as KEYRING_LOCKED, keep the ladder registered for a retry, and evict a rejected agent. - Clearing TP/SL shares the pre-cancel with the replacement path, so a signer failure keeps the protection and fails with KEYRING_LOCKED. - A chase tick whose cancel is rejected evicts the agent. - Attribute a rejected agent to the account it signed for, not the account selected when the rejection arrives. - One helper handles signer failures for every write; the wallet service's isSelectedHardwareWallet becomes requiresSignatureConfirmation. - The controller clears the agents of every provider. - Lighter prepareTradingWallet returns a quiet not-ready result when its session is cancelled; the aggregated provider logs with the provider context convention; TradingService batch logs count only the failures they report. - Tests use the shared agent fixtures and pin the callback arguments.
…through the SDK boundary - Withdrawals and transfers between DEXs fail with KEYRING_LOCKED without logging when the signer cannot sign. - The referral retries after any signer failure, a locked keyring included, so preparation is not reported ready without it. - prepareTradingWallet reports KEYRING_LOCKED when the signer locked while a step failed (HyperLiquid and Lighter); Lighter reports a cancelled session as PROVIDER_LIFECYCLE_STALE. - Cancel batches classify signer failures themselves; #mapError has no side effects, and rejections reported in status entries still drop the agent. - One agent key helper; named patterns; the aggregated provider tags a testnet-pinned Lighter with its network. - The agent-signing integration test mocks only the SDK and drives writes through the controller; more rejection, readiness and referral cases are covered, with exact assertions.
- #mapError no longer carries an unreachable signer branch; every caller classifies signer failures first. - Status-entry rejections of an agent are covered for TWAP and batch cancels; the referral lock tests hold a real lock. - Provider tests assert exact SDK writes, signatures, results and analytics instead of call counts; the builder fee signs its own payload. - Lighter: the first readiness check and the keyring payload encoding are pinned; a stale skipped referral test is removed, with its suppressions.
…s, and serialize referral waiters - A rejected agent the venue reports in cancel status entries fails scale, TP/SL and chase cancels with KEYRING_LOCKED, drops the agent and notifies the host, as a thrown rejection does. - Providers waiting on another's referral attempt take the lock one at a time, so only one writes the referral. - Lighter prepareTradingWallet: a read-only provider (no signer bridge) is ready at once without logging; no selected account returns NO_ACCOUNT_SELECTED; "User cancelled" declines are retried. - TradingService batch logs keep the total failure count and add the reported count. - onAgentRejected is documented as called per rejected write. - Tests cover status-entry rejections, several waiters, address case, re-asks after drops, the Lighter retry and read-only paths, and move the controller binding cases to integration and AgentBindings tests.
…address - A batch cancel whose status entries reject the agent reports it once per signed request. - onAgentRejected receives the agent's address as the host supplied it, not the venue's lowercased form. - prepareTradingWallet docs say which steps the main account signs and that the agent signs the referral and silent migration. - LIGHTER_SIGNER_UNAVAILABLE_ERROR is module-private again. - Tests give each fixture provider its own client service, pin exact calls and results, and cover the one-l "User canceled" decline.
…le, and drop agents rejected on retraction - A host accountSigner that locks while signing and throws its own error now fails with KEYRING_LOCKED (the host error as its cause), so the referral and migration stay retryable (HyperLiquid and Lighter). - Retracting a stale TWAP or an abandoned chase order drops a rejected agent and reports it, whether the rejection is thrown or in a status entry. - The agent resolver no longer carries an unreachable supersede check. - prepareTradingWallet docs on the HyperLiquid provider and the read-only Lighter wording match the controller's. - Tests pin that a failing agent stays in use, two waiters at the referral lock, an unknown wallet without an address, and that every aggregated provider is prepared.
…ocks during builder fee setup - prepareTradingWallet does not ask a wallet with no HyperLiquid account yet to approve the builder fee (the venue rejects its writes). - A builder fee approval skipped because the signer cannot sign fails the write with KEYRING_LOCKED instead of its approval failure code, so a locked TP/SL update is not reported as an error. - HyperLiquid prepareTradingWallet returns NO_ACCOUNT_SELECTED without logging, like Lighter. - The controller's prepareTradingWallet docs say which declined steps are asked again. - Test fakes wrap wallet errors as the SDK does; tests cover the retraction drops, a network switch with a resolved agent, the keyring signing phases and Lighter's recheck after a signed registration.
…report locked signers in every prepare path - A wallet that starts trading setup before its first deposit gets its referral set once it has deposited, instead of after a reconnect. - prepareTradingWallet reports KEYRING_LOCKED when the signer locked during an unfunded wallet's setup, and the controller reports it for a provider with nothing to prepare. - A builder fee signature rejected as locked fails the TP/SL update or order with KEYRING_LOCKED instead of its approval failure code. - The Lighter testnet rule the aggregated provider tags errors with lives in one internal helper. - README documents signing without a KeyringController; the CHANGELOG names the results that changed. - Tests run the KeyringController host shape through the controller, share the SDK error and signing fakes, and pin exact results.
…retry a referral whose code was not ready - prepareTradingWallet returns EXCHANGE_ACCOUNT_NOT_FOUND for a wallet with no HyperLiquid or Lighter account yet, so the host can ask for a deposit instead of another attempt. - A referral skipped because the referral code is not ready, or because the venue rejected the wallet as unknown, is attempted again. - prepareTradingWallet reports a builder fee signature rejected as locked as KEYRING_LOCKED. - Tests run the controller on a real host messenger (a stray keyring call throws), trade on testnet after a network switch, cover a host without onAgentRejected and the keyring readiness fallback, and pin the approval-failure and superseded-rejection paths.
…ion, and report a locked signer for providers without setup - A HyperLiquid referral whose MetaMask referral code is not ready no longer holds trading setup back, so orders stop looking the code up again; the next prepareTradingWallet checks it. A failed lookup is logged once and waits for the next setup. - PerpsController.prepareTradingWallet returns KEYRING_LOCKED when a provider reports ready while the main account cannot sign (for example an aggregated provider whose providers have nothing to prepare). - Document every prepareTradingWallet result, use isProviderOnTestnet for the Lighter provider, and evict rejected agents directly where only eviction is needed. - Split the HyperLiquid account-signer tests into focused files over a shared fixture, and cover a mixed batch cancel, a deselection during Lighter registration and more host error shapes.
…gent, and report stale or unselected preparation - HyperLiquid cancel batches classify every status entry even when one reports a rejected agent, so orders the venue did cancel are no longer counted as resting. A TP/SL update that cancelled one leg restores it and fails with KEYRING_LOCKED; a scale ladder keeps only the rungs left. - HyperLiquid prepareTradingWallet reports PROVIDER_LIFECYCLE_STALE when the provider disconnects during its account check or builder fee setup. - PerpsController.prepareTradingWallet reports NO_ACCOUNT_SELECTED for a provider's ready result while no account is selected, and a read-only Lighter provider checks the selected account first. - Keep each resolved agent next to its getAgentSigner answer in one map, and name the referral code lookup's logs after its method. - Tests: TP/SL and scale mixed-status cancels, mid-setup disconnects, a host without onAgentRejected or whose onAgentRejected throws, distinct suite titles and shared main-account fixtures.
… TP/SL with a new agent, and check the prepared account - HyperLiquid cancel batches read the per-entry statuses of the error the SDK throws when an entry fails before classifying the error, so orders the venue cancelled are no longer reported as resting or failed. cancelOrders gives KEYRING_LOCKED only to the entries that name the rejected agent. - A TP/SL replacement the signer could not sign drops a rejected agent before the old protection is restored; a restoration the signer could not sign is not logged, and the protection is reported lost. - HyperLiquid prepareTradingWallet reports PROVIDER_LIFECYCLE_STALE when the selected account changes during setup; the controller treats the empty account the AccountsController answers with nothing selected as NO_ACCOUNT_SELECTED. - Tests: thrown and returned mixed cancel responses, TP/SL restoration with a new agent, the silent agent migration through the controller, a locked keyring host on Lighter, exact rejection identity, and fixture cleanups.
…stale, and treat unsigned HIP-3 transfers as retryable - PerpsController.prepareTradingWallet compares the selected account before and after the provider's preparation (in aggregated mode, every provider in turn) and returns PROVIDER_LIFECYCLE_STALE when it changed. HyperLiquid's own check treats a deselection during setup as stale too. - A HIP-3 rollback or auto-rebalance transfer that fails with KEYRING_LOCKED is debug-logged with the amount left on the HIP-3 DEX instead of reported as an error. - #classifySignerFailure looks a rejected agent up once. - Docs: the Fixed entry covers the HIP-3 transfers and no longer lists the new accountSigner; getAgentSigner's rejection wording. - Tests: HIP-3 rollback and rebalance with KEYRING_LOCKED and failed transfers, a partial TP/SL cancel whose restoration fails, account changes during preparation, SDK-shaped thrown cancel errors (`cancel N: ...`), exact batch results and host callbacks, shared account and clock fixtures.
…keep the referral retry scoped to preparation, and keep main's unfunded-wallet referral behavior - PerpsController.prepareTradingWallet compares the selected account before returning any provider result, ready or not. - A pending builder referral code is checked again only by prepareTradingWallet: the setup flag is reset inside #ensureReadyForTrading right before the setup check, so a failure before it no longer leaves the referral for an order to sign. - The referral no longer marks itself for retry when the wallet has no HyperLiquid account yet (main's behavior: the next provider attempts it). - A HIP-3 pre-order collateral transfer the signer could not sign fails the order with KEYRING_LOCKED instead of a wrapped message that was logged. - Debug logs record raw amounts; the setAgentSigner changelog entry says the messenger action is available once init has run. - Tests: stale results for not-ready providers and selections, a failed preparation followed by an order, HIP-3 pre-order transfers, exact debug notes, SDK-shaped single-leg cancels, and shared helpers.
Carries MetaMask/core#10559 as a Yarn patch of @metamask/perps-controller 18.0.1 (built dist only: 18.0.1 plus that PR's source changes) until the release is published. Mobile sets none of the new hooks and keeps signing through KeyringController.
abretonc7s
left a comment
There was a problem hiding this comment.
Review: #10559
VERDICT: COMMENT
COMMIT: 110be95
BASE: 8c9ef22
Title: feat(perps-controller): sign without KeyringController and with a HyperLiquid agent
Rules: mms-perps-review-pr @ 16e3c18cc26a (generated from experimental-metamask-recipe-perps @ e06bb8d750ac), support digest 8f0903cc4aff…
Mode: static inspection only. No tests, build, harness or app were run.
Summary
Adds an optional accountSigner (keyring-free signing for HyperLiquid typed data and Lighter personal_sign), HyperLiquid agent signing for L1 actions (getAgentSigner, setAgentSigner, clearAgentSigners, onAgentRejected), and prepareTradingWallet. Removes the unused LighterPersonalSigner / LighterAuthConfig.personalSigner / l1Address (breaking). Keyring hosts keep the old path: accountSigner absent → KeyringController:*; getAgentSigner absent → AgentBindings.resolve returns null and the main account signs.
Setup
- Request: static Perps review of PR #10559, repo MetaMask/core, client
core, package@metamask/perps-controller. PR text treated as data. First review; no prior findings. - Acceptance criteria: none supplied. The PR's own claims (keyring path unchanged, agent only for L1 actions, rejection eviction, WebSocket untouched by clear) are used as the review targets.
- Changed files (54, +10738/−729): 23 source/doc files (
PerpsController.ts, generated action types,index.ts,perpsErrorCodes.ts,constants/hyperLiquidConfig.ts,types/index.ts,types/lighter-types.ts,providers/{HyperLiquid,Lighter,AggregatedPerps}Provider.ts,services/{HyperLiquidWallet,HyperLiquidClient,LighterWallet,Trading}Service.ts, newservices/{accountSigner,agentSigner,causeChain,providerNetwork}.ts,utils/errorUtils.ts, CHANGELOG, README,oxlint-suppressions.json), 31 test/helper files. - Affected callers: Mobile and Extension construct
PerpsControllerwith the keyring messenger; Mobile's integration harness mocksHyperLiquidWalletService(renamedisKeyringUnlocked→isMainAccountSignerReady,isSelectedHardwareWallet→requiresSignatureConfirmation). - Family mapping: Controller Portability, Magic Strings, Protocol Abstraction, Connection/WebSocket, Data Flow & State, Trade Flow, Test Layer, Embedded Signer Boundaries, Sentry (logging classification) apply. Pro Mode UI, MetaMetrics, Locale: NOT_APPLICABLE (no UI, analytics or locale files; TradingService analytics calls unchanged).
Base review
Behavior trace
- Keyring host, no agent:
HyperLiquidWalletService.createWalletAdapter().signTypedData(services/HyperLiquidWalletService.ts:222) →isL1Action→#resolveAgent→ provider#resolveAgentSigner(HyperLiquidProvider.ts:2090) →AgentBindings.resolve(services/agentSigner.ts:95) returns null (no binding, no host callback) → entry deleted →#signWithMainAccount→#signTypedMessage→KeyringController:signTypedMessage. Same as base apart from one extra async hop. accountSignerhost:#signWithMainAccountchecksisReady()before and after a throw, maps toKEYRING_LOCKED(HyperLiquidWalletService.ts:176-198); Lighter mirrors it (LighterWalletService.ts:95-114). The signing address still comes from the selected account.- Agent host: L1 payloads (
primaryType === 'Agent'and domainExchange) go to the agent; user-signed payloads (HyperliquidTransaction:*, includingApproveAgent) never reach it, so an agent cannot approve itself or sign withdrawals. Real-SDK routing and signer recovery are covered inHyperLiquidWalletService.account-signer.test.ts:402-510(Node ≥24.9 only). - Agent cache: keyed
network:mainAddressLower(agentSigner.ts:38). A pending answer that spansclearAgentSignersis discarded via a generation counter and asked again (HyperLiquidProvider.ts:2111-2135). Null and failures aren't cached. - Rejection:
#findRejectedAgentneedsisHyperLiquidUserNotFoundErrorplus the reported address matching an agent this provider recorded in#agentSignedFor. A main account with no venue account never matches, so the unfunded-wallet path (EXCHANGE_ACCOUNT_NOT_FOUND) keeps its base behavior. On eviction: the cache entry is dropped if it still matches, the controller releases a matching binding, and the host is told throughonAgentRejected, with a throwing callback contained. - Error classification: every changed write catch calls
#handleSignerFailurebefore logging, and TradingService skipslogger.errorforKEYRING_LOCKEDresults. Batch cancels now read per-entry statuses from thrown SDK errors (HyperLiquidProvider.ts:9700-9750), and entries the venue did cancel stay successful. - TP/SL update: the cancel moved before the clear/replace branch (
:10731-10766) so both use one batch result. A signer failure with nothing cancelled returnsKEYRING_LOCKEDwithout placing. A partial cancel restores the old protection and returns the surviving IDs. prepareTradingWallet: HL checks readiness first, captures lifecycle generation + address, runs#ensureReadyForTrading(recheckPendingReferral), checks registration, runs builder fee withreportSignerFailure, and rechecks provider/account currency after each await (:14668-14756). The controller adds its own account-change and signer checks (PerpsController.ts:5959-5998). The aggregated provider runs providers sequentially and returns the first not-ready result (AggregatedPerpsProvider.ts:1064).- Retry vs permanent caches: signer failures, including a rejected agent, set
#unifiedAccountSetupNeedsRetry/#referralSetupNeedsRetryand cache nothing. A referral code inpendingsets#referralAwaitsBuilderCode, which onlyprepareTradingWalletre-checks. The cross-provider referral lock now loops until it is free and re-checks the cache under the lock (:15877-15914). #ensureBuilderFeeApprovalnow rethrowsKEYRING_LOCKEDinstead of returning. Its only caller,#ensureBuilderFeeSetup(:2966), catches it: the order path maps it toKEYRING_LOCKEDand the non-blocking path swallows it after a debug log. No other caller exists.
Tests (inspected, not executed)
- Inspected:
PerpsController.agent-signing.integration.test.ts(15 cases: routing, keyring host, pins, re-creation, pre-init binding, rejection + release, prepare),HyperLiquidWalletService.account-signer.test.ts(real SDKsignL1Action/signUserSignedAction+ signer recovery), and the case counts ofHyperLiquidProvider.agent-rejection(25),prepare-trading-wallet(28),LighterProvider.account-signer(18) andagentSigner(8). - PR claims 4,009 passing on Node 24 and 4,005 on Node 22, and says main's suite passes against this source apart from the 2 removed-signer tests. Not verified here. The real-SDK suite is skipped below Node 24.9, so the Node 22 CI job does not exercise the EIP712Domain/signer-recovery path.
Permissions, secrets, dependencies, wiring
- No new dependencies. Core never receives agent key material, only a
signTypedDatainterface, and logs carry addresses,primaryTypeand domain but no payload secrets. setAgentSigneris a messenger action that takes a function-bearing signer. A bound agent that isn't approved for the account fails at the venue, and one approved for another account trades only that account. No privilege escalation found.- No flags, localization or analytics changes.
Signal over noise
- No TODOs, debug
console, ticket keys or tool mentions in source.oxlint-suppressions.jsondrops two suppressions for a rewritten test, which is correct. - Nit: the JSDoc and inline comments are long and repeated across layers. The
prepareTradingWalletcontract appears nearly verbatim inPerpsController.ts(~30 lines), the generated action types,PerpsProviderintypes/index.ts:2162,HyperLiquidProvider.tsandLighterProvider.ts. The comment on#agentSignedForis 9 lines. See finding F1.
Perps criteria ledger
| Family | Rule | Outcome | Evidence |
|---|---|---|---|
| Controller Portability | Platform import in controller | PASS | New files import only package-local modules and @metamask/utils; grep of new services for process./window./react-native found nothing |
| Controller Portability | Deep import from app code | NOT_APPLICABLE | Core-only diff |
| Controller Portability | __DEV__/platform globals |
PASS | None added |
| Controller Portability | New dependency not in DI interface | PASS | Signing added via PerpsPlatformDependencies.accountSigner and providerCredentials.hyperliquid (types/index.ts:1118-1147, 2916-2922) |
| Controller Portability | Breaking the publisher contract | PASS | Additive API. Breaking removal of LighterPersonalSigner is documented; HyperLiquidWalletService renames are internal but mocked by Mobile (PR note). See core overlay |
| Magic Strings | Hardcoded strings/numbers | PASS | L1 EIP-712 shape exported as HYPERLIQUID_L1_ACTION_PRIMARY_TYPE/_DOMAIN_NAME (constants/hyperLiquidConfig.ts:185-195). 4001 and the regex in LighterProvider.ts:1008-1013 are named constants. README default slot 7 matches LIGHTER_DEFAULT_API_KEY_INDEX (constants/lighterConfig.ts:198). Placeholder/timeout/slippage/leverage/precision/URL/cache rules NOT_APPLICABLE |
| Protocol Abstraction | Execution identity from display fields | NOT_APPLICABLE | No order identity changes |
| Protocol Abstraction | Provider identity lost | NOT_APPLICABLE | No fill aggregation changes |
| Protocol Abstraction | Hardcoded provider / provider-specific error handling | PASS | prepareTradingWallet/clearAgentSigners are optional PerpsProvider members. Aggregated provider fans out and tags errors per provider (AggregatedPerpsProvider.ts:1064-1101). The agent is HL-specific and lives in HyperLiquidCredentials |
| Protocol Abstraction | UI branching, symbols, decimals, detailedOrderType | NOT_APPLICABLE | No UI/market changes |
| Pro Mode UI Gating | all | NOT_APPLICABLE | No UI |
| MetaMetrics | all | NOT_APPLICABLE | No event changes |
| Sentry Tracing | Unbounded background trace volume | PASS | No new traces. Signer failures move from logger.error to debugLogger (less Sentry volume). New logger.error sites are unexpected failures only: prepareTradingWallet catch-alls and the aggregated throw |
| Connection & WebSocket | Cleanup owner for in-flight setup | PASS | prepareTradingWallet captures #lifecycleGeneration before awaits and asserts after (HyperLiquidProvider.ts:14677-14719). Lighter maps LighterSessionCancelledError to PROVIDER_LIFECYCLE_STALE |
| Connection & WebSocket | Stale data after async gap | PASS | Address re-read after each await (assertPreparationCurrent, controller addressAtStart compare). The agent is resolved per signature from the fresh selected account |
| Connection & WebSocket | Second lifecycle owner / WS leak | PASS | clearAgentSigners only clears maps (HyperLiquidProvider.ts:14643-14646); no connect/disconnect |
| Connection & WebSocket | mobile-hook rules (throttle, per-component WS, cache invalidation, WebView) | NOT_APPLICABLE | Core-only |
| Data Flow & State | Changed classification leaves old priority rules | PASS | KEYRING_LOCKED widened to agent failures (documented in perpsErrorCodes.ts:97-99). TradingService suppression is keyed on the same code, and batch results keep per-entry errors |
| Data Flow & State | Derived flag lifecycle | PASS | #referralSetupNeedsRetry/#referralAwaitsBuilderCode reset at the start of each #ensureReferralSet and set only on their paths |
| Data Flow & State | Async flow loses cleanup ownership | PASS | Referral lock: every return path calls completeInFlight(). Builder-fee promise map cleaned in finally |
| Data Flow & State | client hook/UI rules | NOT_APPLICABLE | Core-only |
| Trade Flow | Signed bounds collapsed | NOT_APPLICABLE | No RoE/price math changed |
| Trade Flow | Pre-trade checks / post-trade refresh / slippage | PASS | Order path unchanged apart from error classification. TP/SL cancel-before-place ordering kept, and a signer failure never places replacements over live protection |
| Locale | all | NOT_APPLICABLE | No locale files |
| Test Layer | Degenerate fixtures | PASS | Distinct main vs agent keys with signer recovery. Rejection tests use a reported address differing in case from the supplied one (HyperLiquidProvider.agent-rejection.test.ts:1106) |
| Test Layer | Assertions miss behavior | PASS | Integration test asserts which key signed each payload, not only call counts |
| Test Layer | Clock/numeric control | NOT_APPLICABLE | |
| Embedded Signer Boundaries | Key material after boundary | PASS | Core receives no key material; the agent key stays host-side behind PerpsAgentSigner |
| Embedded Signer Boundaries | Navigation policy / bridge messages | NOT_APPLICABLE | No WebView/bridge changes; Lighter signer bridge untouched |
Cross-repository conformity
- Parity (references/parity.md): NOT_APPLICABLE. No screens, hooks or formatters changed.
- Shared package surface: NOT_CHECKED for Extension. No Extension or Mobile checkout at a recorded revision was supplied, so public import compatibility was not inspected. Mobile compatibility rests on the PR's claim (MetaMask/metamask-mobile#36966 Yarn patch; mock renames). This review did not verify it.
Core criteria
| Rule | Item | Outcome | Evidence |
|---|---|---|---|
| Public Controller Contracts | State shape changes without client migration | PASS | No PerpsControllerState change |
| Public Controller Contracts | Method signature changes without compatibility plan | PASS | New methods/actions only (setAgentSigner, clearAgentSigners, prepareTradingWallet, PerpsController.ts:5896-5998). Optional PerpsProvider members. The internal HyperLiquidWalletService renames need Mobile's mock update, which the PR states was done in metamask-mobile#36966 (not verified) |
| Public Controller Contracts | Event name/payload drift | PASS | No events changed |
| Public Controller Contracts | Package export changes without package-level tests | FINDING F1 | New exports (index.ts:74,131,147,337-340,467-468) and the removal of LighterPersonalSigner have no consumer-style assertion through src/index.ts. Tests import from internal paths (tests/helpers/agentFixtures.ts:7, integration test lines 11-24). The only index import is tests/placeholder.test.ts |
| Release Metadata | Public API change without changelog | PASS | CHANGELOG Unreleased: Added/Removed/Fixed in the required order, with the BREAKING entry under Removed and migration guidance (CHANGELOG.md:15-50) |
| Release Metadata | Breaking change released as minor/patch | PASS | No version bump in this PR (package.json 18.0.1). The release PR must ship this as 19.0.0 |
| Release Metadata | Controller and client integration out of sync | NOT_CHECKED | Mobile paired PR linked (#36966). No Extension compatibility note, and no Extension checkout supplied |
| HL Multi-Sig | Probe + catch classifier per user-scoped write | NOT_APPLICABLE | No new exchange write. prepareTradingWallet drives the existing migration, referral and builder-fee writes and keeps their #isWalletOnHyperliquid gates (HyperLiquidProvider.ts:14700-14714, :15851-15862). Agent routing does not change which write is sent |
#ensureUnifiedAccountEnabled cache semantics |
Permanent condition cached as retryable / both flags / deferred treated as failure | PASS | Only transient signer failures (locked, agent unavailable, agent rejected → next resolve gets a new agent) set #unifiedAccountSetupNeedsRetry without caching (HyperLiquidProvider.ts:2697-2708). Permanent and defer paths are unchanged |
| Metered Benefits | Strict improvement | NOT_APPLICABLE | No fee-resolution change |
| Read-only Account Guards | Every write guarded | PASS | Lighter read-only (no signer bridge) prepareTradingWallet returns without writing (LighterProvider.ts:1339-1341). HL has no read-only account mode. The agent can't sign user-signed actions (withdraw, transfers, approveAgent), enforced in isL1Action (HyperLiquidWalletService.ts:54-59) |
| Evidence Expected | Contract matrix, client note, abstraction tests, grep for client imports | PARTIAL / NOT_CHECKED | PR lists every new API and links the Mobile PR, and provider/aggregated tests exist. No Extension note or explicit grep evidence in the PR, so this review ran its own grep of the new files (no client globals) |
Findings
- F1 (minor)
packages/perps-controller/src/index.ts:338: The public surface changes (newPerpsAccountSigner,PerpsAgentSigner,PerpsAgentAccount,PerpsTypedDataPayload, three action types,HYPERLIQUID_L1_ACTION_*, removedLighterPersonalSigner), but no package-level test imports them fromsrc/index.ts. A dropped or misspelled export would pass every internal-path test, and the PR's own harness note shows consumers depend onPerpsAccountSignerbeing exported. Smallest fix: one consumer-style test that imports the new runtime constants and types from../src/index.jsand asserts the constant values, with type-only assignments for the signer shapes. - F2 (nit)
packages/perps-controller/src/PerpsController.ts:5959: TheprepareTradingWalletcontract is written out at length (~30 lines) and repeated nearly verbatim in the generated action type,PerpsProvider.prepareTradingWallet(types/index.ts:2162-2179),HyperLiquidProvider.prepareTradingWalletandLighterProvider.prepareTradingWallet. Similar long comments sit on#agentSignedFor(HyperLiquidProvider.ts:1570-1580) andgetAgentSigner(types/index.ts:1118-1133). Keep the full contract once, on thePerpsProvidertype or the controller method, and cut the others to a line or two plus a reference, so the copies can't drift.
No blocker or major defects found in the traced paths.
Evidence
- Frozen diff
8c9ef2233...110be95a9read for every non-test source file. Key paths traced with file:line above. - Tests inspected, not executed: the agent-signing integration suite, real-SDK wallet-adapter suite, agent-rejection case at
:1106, and the case counts of the prepare/Lighter/agentSigner suites. - PR-reported runtime evidence (testnet harness 5/5 across the keyring, accountSigner and agent modes, agent lifecycle, revocation →
KEYRING_LOCKED+onAgentRejected, Mobile patch run) is recorded as claims only. This review did not reproduce any of it.
Limitations
- Static only. No tests, type-check, lint,
yarn changelog:validateor build were run, so the test counts and coverage claims are unverified. @nktkas/hyperliquidis not installed in this checkout, so the SDK's error-message format andEIP712Domaininclusion were checked only through the repo's tests, not the SDK source. The real-SDK suite is skipped below Node 24.9.- Extension and Mobile checkouts were not supplied: consumer compatibility (the renamed internal wallet-service methods in mocks, the removed Lighter type) is NOT_CHECKED.
- The PR itself notes one gap: whether a wallet will sign an
approveAgentdomain chainId other than its active network was not tested.
Run QA handoff (questions)
- Run
yarn workspace @metamask/perps-controller run teston Node 22 and on Node ≥24.9. Confirm the real-SDK suite runs on 24.9+ and that coverage thresholds hold. - Run
yarn changelog:validateandyarn lint(constraints, knip) for the new exports and the removed type. - Build Extension against this package and check for
LighterPersonalSignerimports andHyperLiquidWalletServicemethod mocks. - On testnet with an agent host, with a revoked agent during an active chase order: confirm each tick calls
onAgentRejectedand the host deduplicates prompts as documented.
Recommended Action
COMMENT. The implementation is careful: the keyring path is unchanged, agents are confined to L1 actions and to the account and network they were resolved for, signer failures are retryable without cache poisoning, and lifecycle and account races are guarded. Before approval: add the package-level export test (F1), optionally trim the duplicated docs (F2), and close the NOT_CHECKED Extension compatibility and unexecuted-test gaps with a Run QA pass.
…entrypoint, and document prepareTradingWallet once
geositta
left a comment
There was a problem hiding this comment.
Approve. Injected signer contracts keep host owned keys separate from provider trading logic. HL routes L1 actions through account and network scoped agents while retaining main account signing for user authorized actions.
## Explanation - Release major perps-controller 19.0.0 Breaking changes (see changelog): - `OrderFill.liquidation.liquidatedUser` is now optional - `FrontendOrder.orderType` union adds `Twap Slice`, `Vault Close`, `Spot Dust Conversion` - Removed `LighterPersonalSigner` and the `personalSigner` / `l1Address` fields of `LighterAuthConfig` ## References - #10414, #10437, #10464, #10559, #10588, #10589, #10591 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them
…36980) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> Lighter orders are signed by a trading key (Lighter's "API key") that the embedded signer registers in a numbered slot of the account, with one `personal_sign` from the wallet. A key only works on the device that created it, so simulators, devices and scripts sharing an account need different slots. When the configured slot already holds another device's key, the app stops with "Lighter API key slot N already contains a different key", which is hard to understand without context. This adds `docs/perps/lighter-api-keys.md` (what a slot is, local setup, how to check a free slot, the error and its fix, testnet resets) and points the `MM_PERPS_LIGHTER_API_KEY_INDEX` comment in `.js.env.example` to it. The perps-controller README gets the same explanation in MetaMask/core#10559. ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> CHANGELOG entry: null ## **Related issues** <!-- mms-check: type=issue-link required=true --> Refs: MetaMask/core#10559 ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> N/A: documentation only. The fix it describes was checked on an iOS simulator against Lighter testnet: with the configured slot held by another device's key the app stopped with the error above, and with `MM_PERPS_LIGHTER_API_KEY_INDEX` set to a free slot the app registered its own key there. ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> N/A: documentation only. ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation and env example comments only; no runtime or security logic changes. > > **Overview** > Adds **`docs/perps/lighter-api-keys.md`** so developers understand Lighter **trading key slots**: how signing works vs the wallet, local env setup (`MM_PERPS_LIGHTER_PROVIDER_ENABLED`, `MM_PERPS_LIGHTER_API_KEY_INDEX`, account index), **one slot per device/simulator/script**, curl checks for free slots, what to do when **"Lighter API key slot N already contains a different key"** appears, and account index behavior after testnet resets. > > Updates the **`MM_PERPS_LIGHTER_API_KEY_INDEX`** comment in **`.js.env.example`** to call it a Lighter trading key slot, note the controller default (**7**), and link to the new doc. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ec3ad2a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->

Explanation
PerpsControllersigns throughKeyringControllermessenger actions, so a client without a keyring (a web trading terminal on an EIP-1193 wallet, later Electron) can't use it. It also asks the main wallet to sign every HyperLiquid order, which an external wallet can't do: HyperLiquid L1 actions use chainId 1337, and wallets reject a typed-data chainId that doesn't match the connected chain.This PR adds two optional hooks and keeps the keyring path unchanged when neither is set:
PerpsPlatformDependencies.accountSigner:signTypedData,signPersonalMessage, optionalisReadyandrequiresSignatureConfirmation. When set, HyperLiquid typed data and Lighterpersonal_signgo through it and noKeyringController:*action is called. The signing address still comes from the selected account.providerCredentials.hyperliquid.getAgentSigner(account)resolves an approved agent key for a main account and network, and L1 actions (orders, cancels, leverage) are signed by it.PerpsController:setAgentSignerbinds or pins an agent explicitly, andPerpsController:clearAgentSignersforgets them, for example on lock.KEYRING_LOCKED; optionalonAgentRejected(account, agentAddress)tells the client.PerpsController:prepareTradingWalletruns the deferred setup (HL migration, builder fee, referral; Lighter key registration) before the first order, so an external or hardware wallet signs it in one session, and reports readiness.Breaking: removes
LighterPersonalSignerandLighterAuthConfig.personalSigner/l1Address. The controller never forwarded them, so they had no effect;accountSigner.signPersonalMessagereplaces them.Agent handling:
References
Validation
3e028d469f, coverage thresholds met, including a test of the new public exports throughsrc/index.ts. The agent-signing integration test runs the real controller, provider and client service against a faked SDK. Every review fix was checked red/green against the reverted sourcemain's own test suite (3,803 tests) run against this branch's source passes, except the 2 tests of the removed Lighter headless signer. The only test-side changes needed were the renamed methods of the internalHyperLiquidWalletServicein mocks (isKeyringUnlocked→isMainAccountSignerReady,isSelectedHardwareWallet→requiresSignatureConfirmation, same answers for keyring hosts) and the SDK error class in one SDK mock110be95a9c(the later commits only add tests and shorten doc comments) (market lifecycle with close, limit lifecycle with cancel, order edit, TP/SL, close position):KeyringControllermessenger (the Mobile and Extension wiring): 5/5 pass, 8 to 11KeyringControllercalls per recipeaccountSigner: 5/5 pass, 0KeyringControllercalls, every signature throughaccountSignerKeyringControllerand 0 main-account signatures; every order, cancel, edit, TP/SL and close signed by the agent keyaccountSignermode onorigin/main(d063750206): every run stops at the harness's own guard, because that checkout does not exportPerpsAccountSigner, before any order. It shows the mode is unavailable onmain, not a difference inmain's trading110be95a9c, with a fresh agent key:accountSigneras a named agent valid for one hour, andextraAgentslisted it with the requestedvalidUntilprepareTradingWalletreturnedready: truewithout any signatureclearAgentSignersleft the one WebSocket open (none closed) while price updates continued, and the next L1 action askedgetAgentSigneragainKEYRING_LOCKED,onAgentRejected(account, agentAddress)was called, and the agent was dropped and asked for againapproveAgentsignature chain: HyperLiquid testnet acceptedapproveAgentsigned with signature chain0x1,0xa4b1(Arbitrum One),0x66eee(Arbitrum Sepolia) and0xaa36a7(Sepolia), each with a matching EIP-712 domain chainId. Whether a wallet signs a domain chainId other than its active network was not tested459592184c(keyring path, chore(perps): patch perps-controller with account and agent signing metamask-mobile#36966): type-check, all Perps unit (9,513) and integration (50) tests, HyperLiquid testnet journeys (smoke, position and order convergence, Pro TP/SL, order edit) and Lighter venue-key registration pass. The Lighter Lite trade journey could not run: its recipe checks an A/B-test flag that is no longer servedClient note: Mobile's Perps integration harness mocks the internal
HyperLiquidWalletService, so the version bump needs the two renamed methods in that mock (done in MetaMask/metamask-mobile#36966).Checklist
Note
High Risk
Changes core trading signing paths (HyperLiquid L1 vs user-signed, Lighter registration), adds agent lifecycle and breaking Lighter config removal, with broad touch on order/cancel/TP-SL error handling.
Overview
Adds optional
PerpsPlatformDependencies.accountSignerso hosts withoutKeyringControllercan sign HyperLiquid EIP-712 and Lighterpersonal_signfrom the selected account, withKEYRING_LOCKEDwhen the signer is not ready and optional deferral of init-time prompts viarequiresSignatureConfirmation.Adds HyperLiquid agent signing for L1 actions only:
getAgentSigner, controllersetAgentSigner/clearAgentSigners, venue rejection handling (onAgentRejected, agent eviction,KEYRING_LOCKEDinstead of “unknown wallet”), plusHyperLiquidWalletServicerouting (agent forExchange/Agenttyped data, main account for user-signed flows). NewprepareTradingWallet(controller, aggregated provider, HyperLiquid, Lighter) runs migration, builder fee, referral, and Lighter key registration ahead of the first order and returnsReadyToTradeResult.Breaking: removes
LighterPersonalSignerandLighterAuthConfig.personalSigner/l1Addressin favor ofaccountSigner. HyperLiquid writes that fail because the signer is unavailable are classified asKEYRING_LOCKED, often without provider/TradingServiceerror logging; batchcancelOrdersreports per-order outcomes when some entries succeed. Docs/changelog and new exports (PerpsAccountSigner,PerpsAgentSigner, L1 EIP-712 constants) accompany the API surface.Reviewed by Cursor Bugbot for commit 3e028d4. Bugbot is set up for automated code reviews on this repo. Configure here.