Skip to content

Add StealthEX swap plugin - #485

Open
j0ntz wants to merge 4 commits into
masterfrom
jon/integrate-stealthex-swap
Open

Add StealthEX swap plugin#485
j0ntz wants to merge 4 commits into
masterfrom
jon/integrate-stealthex-swap

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Adds StealthEX as a centralized swap provider, on the v4 API.

Asana: https://app.asana.com/0/1215088146871429/1217498055202092

Asset identity. StealthEX names every asset by a symbol plus a network, so src/mappings/stealthex.ts records both halves per chain: the symbol and network of the chain's native asset, plus the network its tokens live on. A chain's native asset usually sits on mainnet, but plenty do not (BNB on bsc, AVAX on avax-c, WAXP on wax, OSMO on cosmos, ETH on each L2's own network), which is why the mapping spells out both rather than deriving one from the other.

Token identity comes from the CONTRACT ADDRESS, never from the Edge currency code. The plugin pulls StealthEX's full /v4/currencies catalog (paged at 250, about a thousand listings, page 0 then batches of four), indexes it by provider network, and canonicalizes each listing's contract_address through that wallet's own currencyConfig.getTokenId so the index is keyed by Edge tokenId. Casing differences and junk values in the provider's contract field drop out of that step. A native asset must match the mapped symbol on its network AND carry no contract address, so a token listing can never be resolved for a native swap. A token StealthEX lists without a contract address (USDC on Tron among them) stays unsupported rather than quoted on a ticker match.

The catalog is cached for an hour and replaced atomically, only after every page arrives non-empty. A failed or empty refresh keeps the previous good index and leaves the stamp expired, so the next quote retries instead of caching an outage. The expiry is not theoretical: ARRR and ZANO were both listed on 2026-08-14 and are absent from the catalog on 2026-08-18.

Rate type. Quotes use a fixed rate when both assets support one, which is also what makes "quote by the amount I want to receive" work (StealthEX only offers reversed estimation on fixed rates). Both assets advertising fixed support does not guarantee the pair has a fixed route, so a missing route falls back to a floating quote with isEstimate: true.

fetchSwapQuote runs checkInvalidTokenIds first, so the shared same-asset guard and the default invalid-token list apply here as they do in the sibling central plugins.

Other notes. Requests go through io.fetchCors: StealthEX blocks browser-origin requests, and swap plugins run inside a WebView. Limits come from POST /v4/rates/range and map to SwapBelowLimitError / SwapAboveLimitError; NoPair / NoExchangeRoute / RouteIsDisabled / MarketUnavailable map to SwapCurrencyError and NotAllowed to a reasonless SwapPermissionError: StealthEX returns NotAllowed for causes it does not name (it was observed refusing a route minutes after the identical call succeeded), so claiming a geographic restriction would be a guess.

Telos and Monad are listed by StealthEX but mapped to null: neither listing carries an address format, so which address space they expect cannot be confirmed, and a wrong guess loses funds.

Errors. One helper maps a StealthEX error kind to its Edge error for every call site (/rates/range, /rates/estimated-amount, /exchanges), so a kind cannot mean an unsupported pair on one path and a generic failure on another. A catalog outage raises a real provider error rather than SwapCurrencyError, so it keeps its error ranking instead of reading as an unsupported pair.

Fixed orders always carry a locked rate. A fixed estimate that comes back without rate.id is treated as no fixed route: a direct quote falls back to floating with the range, limit check and estimate re-run for that rate, so isEstimate always describes the order actually created.

Max quotes create one order, not two. getMaxSwappable runs the quote function twice, so the quote path is split: fetchQuote resolves the route, checks limits and returns both addresses without creating anything; fetchProbeOrder builds the probe spendInfo from it (the user's own from-address, skipChecks: true) so getMaxSpendable can price the network fee; and fetchSwapQuoteInner is the only caller that POSTs /exchanges. Above the maximum the probe quotes the range maximum instead of throwing, because StealthEX rejects an out-of-range amount at /rates/estimated-amount with 400 InvalidAmount and a max swap that fits once the fee is subtracted would otherwise never get priced; the real quote still raises SwapAboveLimitError. Measured against the live API, a max quote issued two orders before this and one after.

Trust boundary. The deposit amount StealthEX returns becomes a signed spend, so a from quote (a max quote arrives here as one) rejects an order whose deposit exceeds the requested amount. A reversed quote pins the receive side, so there is nothing local to bound the source against.

This branch is rebased onto #482, which landed AGENTS.md, .cursor/BUGBOT.md, the reworked plugin guide and the plugin template. The overlapping docs commit on this branch was reduced to what #482 does not already cover: three review rules in .cursor/BUGBOT.md (per-endpoint auth, catalog cache expiry, and the scope a fixed-then-floating fallback is allowed to cover) and the rate-type requirement in docs/API_REQUIREMENTS.md that the last of those argues back to the provider.

A compliance and gap report for StealthEX v4 against docs/API_REQUIREMENTS.md is published at https://gist.github.com/j0ntz/ae71f231a5939489cf4611325058c00c

Companion GUI PR wires the plugin up: EdgeApp/edge-react-gui#6160


Note

Medium Risk
New swap path handles real deposits and signed spends with provider-returned amounts; mistakes in mapping, limits, rate fallback, or the trust boundary could misquote or over-debit users, though the implementation follows established central-plugin patterns.

Overview
Adds StealthEX as a new centralized swap provider on the v4 API, registered in src/index.ts as stealthex.

The plugin (src/swap/central/stealthex.ts) pages and caches the /currencies catalog (hourly TTL, batched fetches), resolves assets by contract address via a per-chain token index, then quotes through range → estimate → a single /exchanges order. It prefers fixed rates when both assets advertise them and falls back to floating only when the fixed route is missing (not on limit/pair errors); quoteFor: 'to' uses reversed estimation on fixed routes only. Max swaps use a quote-only probe (skipChecks, no order) before one real order; deposit amounts from the provider are floored to integer native units and capped against the requested from-amount on from quotes.

Chain coverage lives in src/mappings/stealthex.ts (symbol + network per chain, explicit null where support is unconfirmed).

Docs and review guidance are updated: CHANGELOG, new Rate types section in docs/API_REQUIREMENTS.md, and BUGBOT rules for per-endpoint auth, catalog TTL, and scoped rate-type fallback.

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

@j0ntz

j0ntz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence

stealthex quote

stealthex quote

stealthex success

stealthex success

stealthex tx details

stealthex tx details

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz marked this pull request as ready for review August 14, 2026 23:58
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from adc61e1 to b442b38 Compare August 14, 2026 23:58
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/swap/central/stealthex.ts Outdated
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from b442b38 to 5a63ddd Compare August 15, 2026 00:05
Comment thread src/swap/central/stealthex.ts
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from 5a63ddd to 8ae9a4d Compare August 15, 2026 00:13
Comment thread src/swap/central/stealthex.ts
Comment thread src/swap/central/stealthex.ts

@paullinator paullinator left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of the StealthEX swap plugin. Six items, numbered for reference: 1 (must fix), 2 (warning), 3-6 (must fix). Item 1 is the main one and includes a suggested implementation approach.

  1. Must fix - token identity is guessed from the Edge currency code instead of resolved from the contract address.
  2. Warning - EVM chains are selected through a static network table rather than the numeric chain ID.
  3. Must fix - resolved currencies are cached for the lifetime of the plugin with no expiry.
  4. Must fix - every currency lookup failure is reported as an unsupported pair.
  5. Must fix - /rates/range maps NotAllowed differently from the estimate and exchange paths.
  6. Must fix - fixed-rate orders are created without requiring rate_id.

Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/mappings/stealthex.ts
Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts
@j0ntz

j0ntz commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Repo docs added on this branch

docs: add AGENTS.md and .cursor/BUGBOT.md distills the findings that keep recurring on plugin PRs into two files the agents read automatically.

AGENTS.md (repo root): what the repo is, the command table (install/prepare, npm run verify, npm run mapctl), a docs index, and the integration invariants that fail silently or only under production traffic:

  • new central plugins start from src/swap/central/template.ts, and docs/API_REQUIREMENTS.md is read before writing code
  • fetchSwapQuote calls checkInvalidTokenIds even with an empty plugin-specific map
  • token identity resolves by CONTRACT ADDRESS, never by Edge currency code, and a native asset requires the provider listing to carry no contract address either
  • every denominationToNative result is rounded to integer atomic units before it reaches an order, limit or quote
  • provider failures map to the typed swap errors precisely, and the same provider error kind maps identically across the estimate, exchange and range paths
  • an empty-string memo or extra id from a provider is absent, never an EdgeMemo
  • credentials go per endpoint, not globally
  • provider catalog caches need an expiry or a re-fetch-on-miss path

.cursor/BUGBOT.md: the same invariants written as review rules (shared-guards-required, native-amount-rounding, error-classification, per-endpoint-auth, empty-memo-absent, catalog-cache-expiry, typed-catch), plus two known-good patterns the bot should not flag: asOptional accepting JSON null, and the template's raw error logging in fetchSwapQuoteInner.

Issues 1, 3, 4, 5 and 6 of this review are those invariants, and this round's fixup brings the plugin in line with each.

StealthEX API compliance and gap report

Issue 2 (numeric EVM chain id) cannot be fixed on this side, so it is written up rather than coded, together with every other gap against docs/API_REQUIREMENTS.md at tip:

https://gist.github.com/j0ntz/ae71f231a5939489cf4611325058c00c

The gaps, each verified against the live API on 2026-08-18:

  • Asset identification (section 1). Quote and order requests identify an asset by symbol plus a provider network name and nothing else. A contract_address placed inside a route is accepted and silently ignored (byte-identical 200 to the request without it), and no numeric chain id exists in /v4/currencies or /v4/currencies/networks. That is why the plugin keeps a static chain table and builds its own contract index client-side.
  • Transaction status (section 5). GET /v4/exchanges/{id} returns 401 without the API key; the spec requires an unauthenticated status endpoint.
  • Error handling (section 3). One error per response, and both below-minimum and above-maximum come back as InvalidAmount / "Amount is out of range" with no amounts and no direction, so the plugin calls /rates/range before every estimate just to recover the limit.
  • Reporting (section 6). Pagination and date filtering are there, but records carry no terminal-status timestamp and no token or chain identifiers for either leg.

Order identification and the unauthenticated status page pass as-is.

Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts
Comment thread src/swap/central/stealthex.ts
@j0ntz

j0ntz commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

All six items handled. 1: assets now resolve from the full /v4/currencies catalog, indexed by provider network and keyed by the Edge tokenId that each listing's contract_address canonicalizes to through currencyConfig.getTokenId; the currency code no longer participates in token identity. 3: the catalog cache is bounded at one hour, replaced atomically only after a complete non-empty fetch, with the previous index kept and the stamp left expired on a failure. 4: currency resolution no longer collapses into SwapCurrencyError; a catalog outage raises a real provider error and NotAllowed becomes SwapPermissionError. 5: one stealthexError(path, kind, request) helper now serves /rates/range, /rates/estimated-amount and /exchanges. 6: a fixed estimate without rate.id is treated as no fixed route, so a direct quote falls back to floating and isEstimate always describes the order created. 2 needs a StealthEX API change (no numeric chain id exists in their catalog, and a contract_address inside a quote route is accepted and ignored); it is documented with the other gaps at https://gist.github.com/j0ntz/ae71f231a5939489cf4611325058c00c and summarized in the PR comment above.

@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch 3 times, most recently from 4978ebe to 125c91a Compare August 18, 2026 23:43
Comment thread src/swap/central/stealthex.ts
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch 3 times, most recently from 7299094 to 6bd3f85 Compare August 19, 2026 00:12

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6bd3f85. Configure here.

Comment thread src/swap/central/stealthex.ts
j0ntz added 4 commits August 19, 2026 11:07
StealthEX identifies assets by a symbol plus a network, so the chain mapping
spells out both halves for a chain's native asset and the network its tokens
live on. Token symbols are not unique across listings, so each side of a quote
is confirmed against StealthEX's own currency record before quoting.

Quotes use fixed rates when the route offers them, which also covers quoting by
the amount the user wants to receive, and fall back to floating rates
otherwise.
…ntegration surfaced

`.cursor/BUGBOT.md` gains per-endpoint auth, catalog cache expiry, and the scope
a fixed-then-floating fallback is allowed to cover. Each one cost a review round
on this integration and none of them fails loudly.

`docs/API_REQUIREMENTS.md` states the rate-type requirement behind the last of
those: a provider that publishes rate types per ASSET but not per ROUTE leaves
the client attempting a type and inferring support from the failure, which reads
the same as an outage.
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from 6bd3f85 to 2944889 Compare August 19, 2026 18:10
@j0ntz

j0ntz commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: executed swap (TRX to ETH on Base) through the rebuilt plugin

stealthex quote

stealthex quote

stealthex swap success

stealthex swap success

Captured by the agent's in-app test run (build-and-test).

@j0ntz

j0ntz commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

AGENTS.md was added in another PR #482 that didn't merge before this PR. This PR rebased onto the merged #482 on the latest round of changes and builds on top of it to further harden first-time agent swap implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants