Skip to content

fix(perps): user or API wallet does not exist - #9709

Open
abretonc7s wants to merge 4 commits into
mainfrom
TAT-3343-fix-core-user-or-api-wallet-does-n
Open

fix(perps): user or API wallet does not exist#9709
abretonc7s wants to merge 4 commits into
mainfrom
TAT-3343-fix-core-user-or-api-wallet-does-n

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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 HyperLiquidProvider to classify that rejection with
isHyperLiquidUserNotFoundError and downgrade it to a debug log — but only on
the account-setup writes. The order path was never covered, so the raw
exchange string escaped verbatim as OrderResult.error: it is both what the
client renders and what TradingService reports as the failed-trade
error_message. Users saw an exchange internal with no hint that the account
simply needs funding, which is consistent with the 0% recovery rate in TAT-3343.

This PR classifies the rejection on the order path.

  • PERPS_ERROR_CODES gains EXCHANGE_ACCOUNT_NOT_FOUND. This is a breaking
    change
    — see below.
  • #mapError maps the rejection to that code instead of passing the raw
    message through — the existing #errorMappings substring table cannot express
    it, because the message embeds the user's address.
  • #handleOrderError routes this one rejection to debugLogger rather than
    logger.error, matching the policy already applied to every other
    user-scoped exchange write. All other order failures are reported exactly as
    before.

closePosition / editOrder are untouched: a wallet with an open position
necessarily has an exchange account.

Why it's safe

#mapError has a single caller (#handleOrderError), which is reached only
from placeOrder's catch — so the new mapping cannot alter any other public
method's error surface. The else branch preserves the previous logger.error
call verbatim.

Testing

Nine tests added (targeted suites: 89 → 98).

  • Mapping from both rejection sites (updateLeverage, order).
  • Mapping of the non-ok status shape, which HyperLiquid returns more often
    than a thrown rejection and which arrives wrapped as Order failed: {…}.
  • The rejection never reaches logger.error.
  • Unrelated order failures still return their message and still reach
    logger.error.
  • Boundary guard: a near-miss 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.
  • Three tests characterising isHyperLiquidUserNotFoundError, previously
    untested.

Proof is revert-sensitive. Reverting only the two implementation files to main
turns the same suite red (exit 1, 4 failed, 94 passed, 98 total) with the
verbatim ticket symptom in the output; restoring turns it green again.

Root yarn build passes (the exported PerpsErrorCode union changed). ESLint,
Prettier and changelog:validate are clean.

BREAKING — release as a major version

Adding the code widens the exported PerpsErrorCode union, so consumers that
key an exhaustive Record<PerpsErrorCode, …> stop compiling
until they add the
member. Both first-party clients do:

  • Mobile — app/components/UI/Perps/utils/translatePerpsError.ts
  • Extension — ui/components/app/perps/utils/translate-perps-error.ts

The changelog entry is marked **BREAKING:** and carries a migration note. This
must ship as a major version of @metamask/perps-controller, with both client
translations landed in the same rollout. EXCHANGE_ACCOUNT_NOT_FOUND means the
wallet has no HyperLiquid account yet, so the copy should direct the user to fund
the account before trading.

Downstream — required follow-ups

  1. Add the translation entries in both clients (see BREAKING above). The
    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.
  2. Re-key the analytics. The failed-trade error_message becomes
    EXCHANGE_ACCOUNT_NOT_FOUND instead of the raw string, so the Mixpanel
    Perp-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

  • Add the ([#NNNN](…)) PR link to the three CHANGELOG.md entries.
    AGENTS.md requires it, but no PR exists for this branch yet — it is
    unpushed and publication is owned by the gateway — so there is no number to
    link. Deliberately left as (TAT-3343) rather than guessing a number that
    could ship as a dead link. changelog:validate passes without it.
  • Release as a major version and land the Mobile + Extension translation
    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 PerpsErrorCode requires coordinated Mobile/Extension translation updates; order-path error mapping and logging behavior change is scoped to placeOrder via #handleOrderError.

Overview
Breaking: Adds EXCHANGE_ACCOUNT_NOT_FOUND to PERPS_ERROR_CODES so clients with exhaustive error maps must add a translation (fund-account copy).

When placeOrder hits HyperLiquid’s "User or API Wallet 0x… does not exist." rejection (wallet not created until first USDC credit), #mapError now maps it to that code instead of surfacing the raw exchange string on OrderResult.error. #handleOrderError logs 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/order throws and non-ok order status, Sentry suppression, near-miss errors, and isHyperLiquidUserNotFoundError.

Reviewed by Cursor Bugbot for commit 87b523e. Bugbot is set up for automated code reviews on this repo. Configure here.

…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.
@abretonc7s

Copy link
Copy Markdown
Contributor Author

Automated fix-bug run — TAT-3343

Metric Value
Run 4570116c
Duration ?
Model claude/opus
Nudges 0
Worker report

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 HyperLiquidProvider to classify that rejection with
isHyperLiquidUserNotFoundError and downgrade it to a debug log — but only on
the account-setup writes. The order path was never covered, so the raw
exchange string escaped verbatim as OrderResult.error: it is both what the
client renders and what TradingService reports as the failed-trade
error_message. Users saw an exchange internal with no hint that the account
simply needs funding, which is consistent with the 0% recovery rate in TAT-3343.

This PR classifies the rejection on the order path.

  • PERPS_ERROR_CODES gains EXCHANGE_ACCOUNT_NOT_FOUND. This is a breaking
    change
    — see below.
  • #mapError maps the rejection to that code instead of passing the raw
    message through — the existing #errorMappings substring table cannot express
    it, because the message embeds the user's address.
  • #handleOrderError routes this one rejection to debugLogger rather than
    logger.error, matching the policy already applied to every other
    user-scoped exchange write. All other order failures are reported exactly as
    before.

closePosition / editOrder are untouched: a wallet with an open position
necessarily has an exchange account.

Why it's safe

#mapError has a single caller (#handleOrderError), which is reached only
from placeOrder's catch — so the new mapping cannot alter any other public
method's error surface. The else branch preserves the previous logger.error
call verbatim.

Testing

Nine tests added (targeted suites: 89 → 98).

  • Mapping from both rejection sites (updateLeverage, order).
  • Mapping of the non-ok status shape, which HyperLiquid returns more often
    than a thrown rejection and which arrives wrapped as Order failed: {…}.
  • The rejection never reaches logger.error.
  • Unrelated order failures still return their message and still reach
    logger.error.
  • Boundary guard: a near-miss 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.
  • Three tests characterising isHyperLiquidUserNotFoundError, previously
    untested.

Proof is revert-sensitive. Reverting only the two implementation files to main
turns the same suite red (exit 1, 4 failed, 94 passed, 98 total) with the
verbatim ticket symptom in the output; restoring turns it green again.

Root yarn build passes (the exported PerpsErrorCode union changed). ESLint,
Prettier and changelog:validate are clean.

BREAKING — release as a major version

Adding the code widens the exported PerpsErrorCode union, so consumers that
key an exhaustive Record<PerpsErrorCode, …> stop compiling
until they add the
member. Both first-party clients do:

  • Mobile — app/components/UI/Perps/utils/translatePerpsError.ts
  • Extension — ui/components/app/perps/utils/translate-perps-error.ts

The changelog entry is marked **BREAKING:** and carries a migration note. This
must ship as a major version of @metamask/perps-controller, with both client
translations landed in the same rollout. EXCHANGE_ACCOUNT_NOT_FOUND means the
wallet has no HyperLiquid account yet, so the copy should direct the user to fund
the account before trading.

Downstream — required follow-ups

  1. Add the translation entries in both clients (see BREAKING above). The
    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.
  2. Re-key the analytics. The failed-trade error_message becomes
    EXCHANGE_ACCOUNT_NOT_FOUND instead of the raw string, so the Mixpanel
    Perp-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

  • Add the ([#NNNN](…)) PR link to the three CHANGELOG.md entries.
    AGENTS.md requires it, but no PR exists for this branch yet — it is
    unpushed and publication is owned by the gateway — so there is no number to
    link. Deliberately left as (TAT-3343) rather than guessing a number that
    could ship as a dead link. changelog:validate passes without it.
  • Release as a major version and land the Mobile + Extension translation
    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.

@abretonc7s abretonc7s changed the title chore: prepare farmslot publication pkg-4570116c-ms7h33le fix(perps): user or API wallet does not exist Jul 30, 2026
@abretonc7s
abretonc7s marked this pull request as ready for review July 30, 2026 12:13
@abretonc7s
abretonc7s requested review from a team as code owners July 30, 2026 12:13
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)).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant