fix(perps): user or API wallet does not exist - #9709
Conversation
…on as an actionable error code HyperLiquid creates accounts server-side on the first USDC credit. Before that, order-path exchange writes (updateLeverage, order) reject with "User or API Wallet 0x... does not exist.". TAT-2661 taught every other user-scoped exchange write in HyperLiquidProvider to classify that rejection, but the order path was left out, so the raw exchange string was returned verbatim as OrderResult.error - both rendered to the user and reported as the failed-trade analytics error_message. Nothing told users the account simply needed funding, matching the 0% recovery rate reported in TAT-3343. Map the rejection to the new PERPS_ERROR_CODES.EXCHANGE_ACCOUNT_NOT_FOUND so clients can translate it, and keep it out of the error logger like the other pre-account-state paths.
Independent review flagged two gaps in the TAT-3343 coverage:
- HyperLiquid returns a non-ok status more often than it throws, and
#submitOrderWithRollback wraps that as `Order failed: ${JSON.stringify(...)}`.
That shape was only covered by inference; pin it.
- isHyperLiquidUserNotFoundError is substring-based and now gates both error
mapping and logger suppression on the order path, so guard the boundary: an
order error that merely contains "does not exist" must be neither remapped
nor suppressed.
Also note in the changelog that the widened PerpsErrorCode union breaks
consumers keying an exhaustive Record<PerpsErrorCode, ...> until they add the
new member.
Adding EXCHANGE_ACCOUNT_NOT_FOUND widens the exported PerpsErrorCode union, so consumers keying an exhaustive Record<PerpsErrorCode, ...> stop compiling until they add the member. Both first-party clients do: mobile's translatePerpsError.ts and extension's translate-perps-error.ts. Per repo changelog rules that makes it a breaking change and it needs major-version treatment, so mark it and spell out the migration.
Automated fix-bug run — TAT-3343
Worker reportWhatHyperLiquid creates accounts server-side on the first USDC credit. Before that, TAT-2661 taught most of This PR classifies the rejection on the order path.
Why it's safe
TestingNine tests added (targeted suites: 89 → 98).
Proof is revert-sensitive. Reverting only the two implementation files to Root BREAKING — release as a major versionAdding the code widens the exported
The changelog entry is marked Downstream — required follow-ups
Pre-merge checklist
Scope noteThis PR makes the failure legible and actionable. It does not, by itself, reduce |
The Check changelog CI job requires each entry to reference the pull request that introduced it. The PR did not exist when these entries were written, so they carried only the ticket reference. Follows the existing style in this file: (TAT-XXXX) ([#NNNN](url)).
What
HyperLiquid creates accounts server-side on the first USDC credit. Before that,
every user-scoped exchange write rejects with the raw string
"User or API Wallet 0x… does not exist.".TAT-2661 taught most of
HyperLiquidProviderto classify that rejection withisHyperLiquidUserNotFoundErrorand downgrade it to a debug log — but only onthe account-setup writes. The order path was never covered, so the raw
exchange string escaped verbatim as
OrderResult.error: it is both what theclient renders and what
TradingServicereports as the failed-tradeerror_message. Users saw an exchange internal with no hint that the accountsimply needs funding, which is consistent with the 0% recovery rate in TAT-3343.
This PR classifies the rejection on the order path.
PERPS_ERROR_CODESgainsEXCHANGE_ACCOUNT_NOT_FOUND. This is a breakingchange — see below.
#mapErrormaps the rejection to that code instead of passing the rawmessage through — the existing
#errorMappingssubstring table cannot expressit, because the message embeds the user's address.
#handleOrderErrorroutes this one rejection todebugLoggerrather thanlogger.error, matching the policy already applied to every otheruser-scoped exchange write. All other order failures are reported exactly as
before.
closePosition/editOrderare untouched: a wallet with an open positionnecessarily has an exchange account.
Why it's safe
#mapErrorhas a single caller (#handleOrderError), which is reached onlyfrom
placeOrder's catch — so the new mapping cannot alter any other publicmethod's error surface. The
elsebranch preserves the previouslogger.errorcall verbatim.
Testing
Nine tests added (targeted suites: 89 → 98).
updateLeverage,order).okstatus shape, which HyperLiquid returns more oftenthan a thrown rejection and which arrives wrapped as
Order failed: {…}.logger.error.logger.error."Asset BTC does not exist on this DEX")is neither remapped nor suppressed — the classifier is substring-based and now
gates both mapping and logging.
isHyperLiquidUserNotFoundError, previouslyuntested.
Proof is revert-sensitive. Reverting only the two implementation files to
mainturns the same suite red (exit 1,
4 failed, 94 passed, 98 total) with theverbatim ticket symptom in the output; restoring turns it green again.
Root
yarn buildpasses (the exportedPerpsErrorCodeunion changed). ESLint,Prettier and
changelog:validateare clean.BREAKING — release as a major version
Adding the code widens the exported
PerpsErrorCodeunion, so consumers thatkey an exhaustive
Record<PerpsErrorCode, …>stop compiling until they add themember. Both first-party clients do:
app/components/UI/Perps/utils/translatePerpsError.tsui/components/app/perps/utils/translate-perps-error.tsThe changelog entry is marked
**BREAKING:**and carries a migration note. Thismust ship as a major version of
@metamask/perps-controller, with both clienttranslations landed in the same rollout.
EXCHANGE_ACCOUNT_NOT_FOUNDmeans thewallet has no HyperLiquid account yet, so the copy should direct the user to fund
the account before trading.
Downstream — required follow-ups
compile break is the intended failure mode — it forces the translation rather
than silently rendering a bare code — but it must be scheduled, not
discovered.
error_messagebecomesEXCHANGE_ACCOUNT_NOT_FOUNDinstead of the raw string, so the MixpanelPerp-reliability board that motivated this ticket must be updated in the same
rollout — otherwise the metric dropping to zero is indistinguishable from a
reporting gap.
Pre-merge checklist
([#NNNN](…))PR link to the threeCHANGELOG.mdentries.AGENTS.mdrequires it, but no PR exists for this branch yet — it isunpushed and publication is owned by the gateway — so there is no number to
link. Deliberately left as
(TAT-3343)rather than guessing a number thatcould ship as a dead link.
changelog:validatepasses without it.entries in the same rollout.
Scope note
This PR makes the failure legible and actionable. It does not, by itself, reduce
the number of orders that hit this state — that requires the client to act on
the new code. The link between this rejection and the 25% failed-trade share is
a code-reading hypothesis; no Mixpanel or Sentry data was queried while
preparing the change.
Screenshots/Recordings
Programmatic (non-visual) evidence for TAT-3343. This is a headless library package with no UI, so proof is a root build transcript plus Jest transcripts captured by the mm-harness core adapter across a three-phase revert-sensitivity run: green on the committed fix, red with only the two implementation files reverted to main (exposing the exact raw exchange string from the ticket), green again after restoration.
Note
Medium Risk
Breaking API change to
PerpsErrorCoderequires coordinated Mobile/Extension translation updates; order-path error mapping and logging behavior change is scoped toplaceOrdervia#handleOrderError.Overview
Breaking: Adds
EXCHANGE_ACCOUNT_NOT_FOUNDtoPERPS_ERROR_CODESso clients with exhaustive error maps must add a translation (fund-account copy).When
placeOrderhits HyperLiquid’s"User or API Wallet 0x… does not exist."rejection (wallet not created until first USDC credit),#mapErrornow maps it to that code instead of surfacing the raw exchange string onOrderResult.error.#handleOrderErrorlogs this case at debug only, aligning with other user-scoped exchange writes and avoiding Sentry noise; other order failures are unchanged.Tests cover mapping from
updateLeverage/orderthrows and non-ok order status, Sentry suppression, near-miss errors, andisHyperLiquidUserNotFoundError.Reviewed by Cursor Bugbot for commit 87b523e. Bugbot is set up for automated code reviews on this repo. Configure here.