Skip to content

[pull] main from MetaMask:main - #886

Merged
pull[bot] merged 4 commits into
Reality2byte:mainfrom
MetaMask:main
Sep 30, 2026
Merged

pull[bot] merged 4 commits into
Reality2byte:mainfrom
MetaMask:main

Conversation

@pull

@pull pull Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

hmalik88 and others added 4 commits September 29, 2026 22:10
## Explanation

- Adds `@metamask/profile-controller`, a new package for managing
MetaMask user profile state
- `ProfileService` communicates with the MetaMask Profile API
(`profile.api.cx.metamask.io`), exposing `getProfile`, `createProfile`,
`replaceProfile`, `updateProfile`, `deleteProfile`,
`checkUsernameAvailability`, `getXAuthUrl`, `connectX`, and
`getXAccount` via the messenger, with superstruct validation on all
inputs and responses
- `ProfileController` manages profile state derived from the API and
exposes all operations via the messenger; also exports `useGetProfile`
and `useCheckUsernameAvailability` React hooks for UI components via
`@metamask/react-data-query`

## References

N/A

## 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)
- [x] 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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds authenticated profile CRUD, persisted user state, and X OAuth
flows; mistakes could affect account identity data, though
inputs/outputs are struct-validated and heavily tested.
> 
> **Overview**
> Introduces **`@metamask/profile-controller`**, a new monorepo package
for MetaMask user profiles (username, bio, avatar, linked addresses,
trading privacy) and optional **X (Twitter) linking**.
> 
> **`ProfileService`** (extends `BaseDataService`) calls the MetaMask
Profile API with bearer auth from
`AuthenticationController:getBearerToken`, validates requests/responses
with superstruct, and exposes CRUD, username availability, and X OAuth
(`getXAuthUrl`, `connectX`, `getXAccount`) via messenger actions.
> 
> **`ProfileController`** keeps persisted `profile` / optional
`xProfile` state, maps API shapes to UI-friendly types, and delegates
mutations to `ProfileService` (including guards when no profile exists
and clearing state on delete). Broad Jest coverage is included for both
layers.
> 
> Repo wiring adds CODEOWNERS (`@MetaMask/accounts-engineers`),
README/package graph entries, root/tsconfig references, `teams.json`,
and oxlint suppression for the new jest config.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
dc46832. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…erLiquid agent (#10559)

## Explanation

`PerpsController` signs through `KeyringController` messenger 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`, optional `isReady` and
`requiresSignatureConfirmation`. When set, HyperLiquid typed data and
Lighter `personal_sign` go through it and no `KeyringController:*`
action is called. The signing address still comes from the selected
account.
- **HyperLiquid agent signing**:
- `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.
- User-signed actions (builder fee, withdraw) always stay on the main
account.
- `PerpsController:setAgentSigner` binds or pins an agent explicitly,
and `PerpsController:clearAgentSigners` forgets them, for example on
lock.
- An agent the venue rejects (revoked or expired) is dropped and the
write fails with `KEYRING_LOCKED`; optional `onAgentRejected(account,
agentAddress)` tells the client.
  - Approving the agent stays the client's job.
- **`PerpsController:prepareTradingWallet`** runs 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 `LighterPersonalSigner` and
`LighterAuthConfig.personalSigner` / `l1Address`. The controller never
forwarded them, so they had no effect;
`accountSigner.signPersonalMessage` replaces them.

Agent handling:

- the agent is bound to the main account and network it was approved
for;
- agent resolution is race-safe across account and network switches;
- clearing agents doesn't tear down WebSockets;
- the action types are generated rather than hand-edited.

## References

- Mobile PR carrying this change as a Yarn patch:
MetaMask/metamask-mobile#36966

## Validation

- Unit and integration tests for the account-signer and agent paths:
4,012 passing on Node 24 and 4,008 on Node 22 at the head `3e028d469f`,
coverage thresholds met, including a test of the new public exports
through `src/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 source
- Regression check: `main`'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 internal `HyperLiquidWalletService` in mocks
(`isKeyringUnlocked` → `isMainAccountSignerReady`,
`isSelectedHardwareWallet` → `requiresSignatureConfirmation`, same
answers for keyring hosts) and the SDK error class in one SDK mock
- HyperLiquid testnet through the core harness on `110be95a9c` (the
later commits only add tests and shorten doc comments) (market lifecycle
with close, limit lifecycle with cancel, order edit, TP/SL, close
position):
- `KeyringController` messenger (the Mobile and Extension wiring): 5/5
pass, 8 to 11 `KeyringController` calls per recipe
- `accountSigner`: 5/5 pass, 0 `KeyringController` calls, every
signature through `accountSigner`
- agent: 5/5 pass, 0 `KeyringController` and 0 main-account signatures;
every order, cancel, edit, TP/SL and close signed by the agent key
- `accountSigner` mode on `origin/main` (`d063750206`): every run stops
at the harness's own guard, because that checkout does not export
`PerpsAccountSigner`, before any order. It shows the mode is unavailable
on `main`, not a difference in `main`'s trading
- Agent lifecycle on HyperLiquid testnet on `110be95a9c`, with a fresh
agent key:
- the main account approved it through `accountSigner` as a named agent
valid for one hour, and `extraAgents` listed it with the requested
`validUntil`
  - `prepareTradingWallet` returned `ready: true` without any signature
- `clearAgentSigners` left the one WebSocket open (none closed) while
price updates continued, and the next L1 action asked `getAgentSigner`
again
- after the agent was revoked (zero address, same name), the next order
failed with `KEYRING_LOCKED`, `onAgentRejected(account, agentAddress)`
was called, and the agent was dropped and asked for again
- `approveAgent` signature chain: HyperLiquid testnet accepted
`approveAgent` signed with signature chain `0x1`, `0xa4b1` (Arbitrum
One), `0x66eee` (Arbitrum Sepolia) and `0xaa36a7` (Sepolia), each with a
matching EIP-712 domain chainId. Whether a wallet signs a domain chainId
other than its active network was not tested
- Mobile with this change as a Yarn patch of `459592184c` (keyring path,
MetaMask/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 served

Client 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

- [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)
- [x] 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

<!-- CURSOR_SUMMARY -->
---

> [!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.accountSigner`** so hosts
without `KeyringController` can sign HyperLiquid EIP-712 and Lighter
`personal_sign` from the selected account, with **`KEYRING_LOCKED`**
when the signer is not ready and optional deferral of init-time prompts
via **`requiresSignatureConfirmation`**.
> 
> Adds **HyperLiquid agent signing** for L1 actions only:
**`getAgentSigner`**, controller **`setAgentSigner` /
`clearAgentSigners`**, venue rejection handling (**`onAgentRejected`**,
agent eviction, **`KEYRING_LOCKED`** instead of “unknown wallet”), plus
**`HyperLiquidWalletService`** routing (agent for `Exchange`/`Agent`
typed data, main account for user-signed flows). New
**`prepareTradingWallet`** (controller, aggregated provider,
HyperLiquid, Lighter) runs migration, builder fee, referral, and Lighter
key registration ahead of the first order and returns
**`ReadyToTradeResult`**.
> 
> **Breaking:** removes **`LighterPersonalSigner`** and
**`LighterAuthConfig.personalSigner` / `l1Address`** in favor of
**`accountSigner`**. HyperLiquid writes that fail because the signer is
unavailable are classified as **`KEYRING_LOCKED`**, often without
provider/`TradingService` error logging; batch **`cancelOrders`**
reports per-order outcomes when some entries succeed. Docs/changelog and
new exports (**`PerpsAccountSigner`**, **`PerpsAgentSigner`**, L1
EIP-712 constants) accompany the API surface.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3e028d4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Monte Lai <monte.lai@consensys.net>
…orders (#10588)

## Explanation

`TradingService.flipPosition` sends leverage with no `marginMode`.
`HyperLiquidProvider.#prepareAssetForTrading` then fell back to isolated
and called `updateLeverage({ isCross: false })`. On a Cross position,
HyperLiquid rejects that with "Cannot switch leverage type with open
position". On Mobile, every one of these failures (64 events from 20
users over 30 days) was a position flip.

When an order omits `marginMode`, HyperLiquid now uses the open
position's mode. It reads positions from the fresh WebSocket cache and
falls back to one REST read. With no position, or if the read fails, it
keeps the old isolated default. Orders that set `marginMode` explicitly
are unchanged.

The fix is in the HyperLiquid provider, not in `flipPosition`: Lighter
rejects any order that sets `marginMode`, so passing
`position.leverage.type` from the shared service would break Lighter
flips.

## References

- Fixes
[TAT-3994](https://consensyssoftware.atlassian.net/browse/TAT-3994)

## 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


[TAT-3994]:
https://consensyssoftware.atlassian.net/browse/TAT-3994?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
…t reconnect (#10589)

## Explanation

Mobile reconnects Perps with `PerpsController.disconnect()`, a short
cleanup delay, then `init()`. That covers a foreground resume after a
failed ping, and account or network changes.
`#getActiveProviderWhenReady` waited for the disconnect but not for the
`init()` that follows it. An order submitted in that window woke up to
an uninitialized controller and failed with `CLIENT_NOT_INITIALIZED`.

This PR changes only the controller:

- If an action waited on a `disconnect()` and the controller is still
uninitialized afterwards, it waits up to
`PERPS_CONSTANTS.ConnectionTimeoutMs` for the client's `init()` to
start, then runs. If no `init()` follows, it fails as before. The
controller never starts a connection itself.
- If the reconnect switched the selected account, the network or the
active provider, the action fails with `PROVIDER_LIFECYCLE_STALE`
instead of running under the new context. The context is recorded when
the action is issued.

Behavior change: after a final disconnect with no follow-up `init()`, an
action that was already in flight now takes up to 10 s to fail instead
of failing right away. Actions issued after the disconnect are
unaffected.

The provider-side part of TAT-4041 (order-path reads over HTTP during a
WebSocket reconnect, the `getMaxLeverage` 3x cap, 429 retry) is split
into the stacked draft #10590. It stays in draft until a transport
stress test settles the long-term transport choice.

Not in this PR:

- `PROVIDER_LIFECYCLE_STALE` for an order already in flight inside the
provider when the account changes. Continuing under the new account
would be wrong. How that code is shown to the user is up to the client.

## Reproduction

The real headless `PerpsController`, SDK transports and signer ran
against HyperLiquid testnet, with a client driver calling the real
`disconnect()` and a delayed `init()`.

| Controlled condition | Unfixed source (`1f15b7c`) | With the
controller change |
| --- | --- | --- |
| Client `disconnect()` followed by delayed `init()` | Disconnect
finished at 2,970 ms, and the order failed with `CLIENT_NOT_INITIALIZED`
at 2,970 ms. `init()` only started at 3,470 ms. | `init()` finished at
5,260 ms. Order `61421723850` succeeded at 7,882 ms, was observed open,
and was canceled by ID. 0 BTC orders and 0 positions after teardown. |

That run was recorded on `9eb852d`, which also contained the provider
changes now in #10590. The controller code on this path is the same
apart from narrowing: the wait now only follows a `disconnect()`, not a
reinitialization. I have not rerun the testnet reproduction on
`9149042`. No Mobile or Extension UI was exercised.

## Validation (`9149042`)

- `PerpsController` suites: `8 passed`, `465 passed`.
- The 4 new lifecycle tests all fail on `main`: order placed across
disconnect then init, bounded wait, and refusal after an account switch
or a network switch. With only the context guard removed, just the 2
switch tests fail.
- `oxlint` and `oxfmt` clean; `changelog:validate` passes; the package
build type-checks.

## References

- Fixes
[TAT-4041](https://consensyssoftware.atlassian.net/browse/TAT-4041)
(controller part). Provider part: #10590
- Related:
[TAT-4013](https://consensyssoftware.atlassian.net/browse/TAT-4013)
(`PROVIDER_LIFECYCLE_STALE` on account switch),
[TAT-3871](https://consensyssoftware.atlassian.net/browse/TAT-3871)
(retry and idempotency of the exchange call)

## 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


[TAT-4041]:
https://consensyssoftware.atlassian.net/browse/TAT-4041?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
@pull pull Bot locked and limited conversation to collaborators Sep 30, 2026
@pull pull Bot added the ⤵️ pull label Sep 30, 2026
@pull
pull Bot merged commit 707a74c into Reality2byte:main Sep 30, 2026
5 of 11 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants