Skip to content

Add Swapter swap provider - #475

Open
j0ntz wants to merge 1 commit into
masterfrom
jon/swapter-integration
Open

Add Swapter swap provider#475
j0ntz wants to merge 1 commit into
masterfrom
jon/swapter-integration

Conversation

@j0ntz

@j0ntz j0ntz commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Asana task

Adds the Swapter centralized swap provider.

Recreates partner PR #455 (by @markovo4, from markovo4/edge-exchange-plugins) on an
EdgeApp branch so it can run CI, rebased from its v2.46.0 merge-base onto current
master. The plugin design, mapping and test cases are the partner's work; this branch
brings the integration up to current conventions and fixes what had gone stale.

Asana: https://app.asana.com/1/9976422036640/project/1213880789473005/task/1216571782597915

Changes on top of the partner branch

Chain mappings corrected against the live /data/coins list. Swapter has renamed
several networks since the partner branch was cut in May, and nine of the 43 non-null
mappings no longer resolve:

Edge plugin Partner Live network
arbitrum ARBITRUM ARB
optimism OPTIMISM OP
polygon POLYGON POL
solana SOLANA SOL
ethereumpow ETHW ETHEREUM POW (note the space)
bitcoinsv BSV BCHSV (mapped via SPECIAL_MAINNET_CASES)
coreum CORE (none)
fio FIO (none)
telos TELOS TELOS (relisted since; asset TLOS)

A stale code here is caught by no type and no test: a mainnet quote still reaches the API
and fails with Swapter's "Deposit coin and network combination does not exists.", while
a token quote throws SwapCurrencyError because /data/coins returns no ticker set for
the unknown network. Verified by driving the plugin against the live API — polygon at
POLYGON fails min-amount with HTTP 400, and at POL returns HTTP 200. Bitcoin SV needs more than a
mapping entry: getChainAndTokenCodes derives the mainnet ticker from
currencyInfo.currencyCode (BSV) while Swapter lists both the network and the asset as
BCHSV, so it carries a SPECIAL_MAINNET_CASES entry (added in the second review round
below).

Max swaps now resolve. Every other central plugin routes its quote through
getMaxSwappable; this one did not, so its quoteFor !== 'from' guard threw
SwapCurrencyError for every max ("send all") swap and Swapter silently vanished from the
provider list. It now follows the nym pattern: a quote-only probe order prices fees
against the user's own refund address, so a max swap does not create an order that is
immediately abandoned, and exactly one order is created per quote.

Dropped the estimate round trip. /v2/swap/estimate was called and cleaned on every
quote, but its response was only logged — toNativeAmount comes from the create
response. The probe needs only a spendInfo to price fees, and create returns the
authoritative amounts, so neither path needs it.

Regenerated test/partnerJson/swapterMap.json from /data/coins. The partner fixture
held 292 networks, 156 of which are not Swapter networks at all (ERC20, ARBITRUMONE,
AVAXCCHAIN, and a bare A), while omitting 72 real ones — POL was absent entirely.
Every other fixture in test/partnerJson/ is a subset of its provider's mapped codes; this
one now is too (38 networks, 729 tickers).

Dropped unrelated dependency churn: a self-referential "edge-exchange-plugins": "^2.46.0"
dependency, a typescript ~4.9.5^6.0.3 upgrade, a new ts-node devDependency, and
their yarn.lock fallout. This branch touches no dependency files.

Also registered swapter alphabetically in src/index.ts and dropped the whitespace-only
edits to partnerJson.test.ts.

Testing

verify-repo.sh passes (prepare, eslint, tsc, mocha — 18 partnerJson cases including 3
new Swapter ones, asserting against the regenerated live fixture).

An in-app swap has been executed end to end. Two runs, both on the iOS sim against
the live API with the real plugin linked into the app via updot:

  • ETH (Arbitrum) → LTC, reached the swap-success scene.
  • LTC → DOGE, 2026-08-03: quote 0.56602 LTC → 346.92 DOGE rendered as Powered by
    Swapter
    , confirmed through the slider, terminal success scene
    ("Congratulations! Your exchange is being processed!"), wallet balance moved
    1.58639 → 1.01958971 LTC (0.5668 debited, matching the quote plus network fee) and the
    transaction list recorded Swap Funds / Exchange:To DOGE.
  • LTC → DGB, 2026-08-07 (the deposit-range build): quote 0.77014 LTC → 9,499.5 DGB
    rendered as Powered by Swapter, confirmed through the slider, terminal success scene
    ("Congratulations! Your exchange is being processed!"), and the transaction record
    reads Swap Funds / Exchange:To DGB / 0.77014 Ł (+0.000781 fee) / Fixed Quote — the
    Fixed Quote label confirming the type: 'fix' path rather than the float fallback.
    Screenshots are attached in a PR comment.

asSwapterCreateResponse has therefore been validated against real create payloads.
/data/coins no longer rejects a request carrying an X-API-KEY (Swapter fixed that on
their side), but the key-stripping stays in place as cheap insurance against a revert.

Static checks: tsc clean and 59 mocha cases pass, including the Swapter partnerJson
cases asserting against the regenerated live fixture.

Review-round changes (addressing @peachbits)

  • ceil for the below-limit native minimum (never understate Swapter's floor) and floor for the payout toNativeAmount (never over-promise the receive amount), replacing round.
  • Rate selection follows the sibling convention: try a fixed-rate order, fall back to float. Dropped the userSettings.swapType selection (the GUI never passes it, which pinned the plugin to float). Swapter 500s type: 'fixed' for pairs it cannot fix, so that create error is suppressed and the float fallback runs.
  • Fallback correctness: fall back to float only when Swapter rejected the fixed create with no order (a SwapterCreateRejected sentinel), so a create-then-parse-failure never spawns a second order; unsupported-pair errors propagate directly with no retry; rethrow the fixed error only when float was also cleanly rejected, else surface the float error so a live order is not masked.
  • skipChecks: true on the max-fee probe so EVM max swaps no longer fail with SpendToSelfError (matches the NYM fix).
  • SPECIAL_MAINNET_CASES pins native TON to Swapter's GRAM coin (audited all mapped networks; TON was the only native-coin mismatch).
  • Deposit-echo assertion (suggested) left out: Swapter float64-parses the deposit, so a strict eq would false-reject 18-decimal orders, and no enforced max exists to guard.

Second review round (2026-08-03)

Fixed-rate orders actually work now; we were sending the wrong enum. Swapter's create
endpoint spells the fixed type fix, not fixed. An unrecognized value is not rejected
as a bad request — the server answers HTTP 500 io.swapter.controller.swap.factory:7,
which is indistinguishable from a pair it genuinely cannot fix. The fixed-then-float
fallback below therefore fired on every quote, so Edge only ever surfaced floating
Swapter quotes, which lose to fixed quotes when ranking providers. A/B against the live
API with identical bodies:

info.type "fixed" -> HTTP 500 io.swapter.controller.swap.factory:7   (7/7 pairs)
info.type "fix"   -> HTTP 200 {"type":"fix", ...}                    (ETH→LTC, BTC→ETH, ETH→BTC, XMR→LTC)

Driving the real plugin now issues exactly one create call carrying "type":"fix" and
takes no float fallback. The fallback itself is unchanged and still covers pairs Swapter
cannot fix.

Four more chains, plus Bitcoin SV. Re-reconciled every mapping against live
/data/coins (912 assets, 233 networks) after the partner refreshed their own file. Added
what live confirms and rejected what it contradicts:

Edge plugin Swapter network Asset
telos TELOS TLOS
thorchainrune RUNE RUNE
hyperevm HYPEREVM HYPE
monad MONAD MON
bitcoinsv BCHSV BCHSV (via SPECIAL_MAINNET_CASES)

Each was confirmed to quote in both directions through min-amount, and each resolves to
those exact codes when driven through the real plugin. The partner's own file maps
bitcoinsv to BSV, which does not exist on /data/coins and fails min-amount with
code 1; coreum stays null on both sides because Swapter's CORE network is
ambiguous between Cosmos Coreum and Core DAO and a wrong guess misroutes deposits.

Third review round (2026-08-07) — partner's Edge adapter endpoints

Swapter shipped Edge-specific adapter routes and asked us (Slack #swapter, 2026-08-07) to
move onto them and to treat deposit-range as the source of truth. Both are now done, and
the switch fixes a real user-facing bug rather than being a cosmetic endpoint swap.

/v2/swap/min-amount was reporting a floor that create does not honour. The two
disagree on every pair sampled, and min-amount is always the lower of the two, so an
amount could clear this plugin's own minimum check and then be rejected by create with a
generic provider error. Live on ETH→LTC:

/v2/swap/min-amount            -> 0.01415300
/adapter/edge/swap/deposit-range -> {"min":"0.0160060","max":"5239.5248798"}
create at 0.015 ETH            -> HTTP 400 factory:6 "lower than allowed minimum", min 0.0160060

create's own rejection floor tracks deposit-range to seven decimals, not min-amount.
The min-amount call is gone; deposit-range replaces it. The adapter route also
serializes every numeric field as a string, where the /v2 route returns unquoted JSON
numbers that lose precision in JSON.parse.

A maximum is now enforced. deposit-range returns a ceiling as well as a floor, so an
oversized amount raises SwapAboveLimitError with the real limit instead of creating an
order that cannot fill. Both bounds round INWARD into native units (minimums up, maximums
down) so the rounding can never widen the range past what Swapter accepts. The
getMaxSwappable probe does not enforce the ceiling — it deliberately quotes the whole
pre-fee balance, so an above-range balance must clamp through getMaxSpendable rather than
throw on an amount the user never asked to send (same carve-out as nym).

create's two limit codes are mapped. Swapter split what used to be a single
factory:6 into distinct codes, which is what lets the client tell a floor from a ceiling
without string-matching their English message:

Code Meaning Edge error
io.swapter.controller.swap.factory:6 below minimum SwapBelowLimitError (carries the body's min)
io.swapter.controller.swap.factory:9 above maximum SwapAboveLimitError (carries the body's max)

This matters because the range is quoted live and moves with the rate, so an amount that
cleared the range check can still be out of bounds by the time create runs. Both errors
describe the amount rather than the swap type, so they propagate directly with no float
retry.

instanceof never matched an edge-core-js swap error. Its error constructors return a
plain Error carrying a name instead of this, so error instanceof SwapCurrencyError
was permanently false and the unsupported-pair short-circuit added in the previous round
never fired — every such rejection was wrapped and spent a second, doomed create on the
float fallback. Matching on name (the field core's own asMaybeSwap*Error cleaners key
off) fixes it and covers the two new limit errors as well.

No estimate call was added. The same Slack message asks for
/adapter/edge/swap/estimate over /v2/swap/estimate; this plugin calls neither, since
create returns the authoritative amounts alongside the deposit address. Adding one would
be an extra round trip per quote, and its deposit.minimum field reports the stale
min-amount value rather than the deposit-range floor.

New test/swapter.test.ts (6 cases) covers the endpoint switch, both range bounds,
the probe's max carve-out, and the two create limit-code mappings against a faked io.

Follow-ups

  • Verified a real swap end-to-end with a working test key (ETH on Arbitrum to LTC, executed in-app to the success scene). asSwapterCreateResponse validated against live create responses.
  • Consider a mapctl synchronizer for Swapter. /data/coins needs no API key, so it is a
    clean fit, and it would catch renames like POLYGONPOL automatically instead of
    silently dropping a chain. Not done here: mapctl update-mappings constructs every
    provider's synchronizer eagerly and aborts without all of their API keys, so the
    generated-mapping path could not be run or validated in this environment.
  • swapter is registered but is not yet enabled anywhere — the info server serves no
    Swapter init options, so the GUI needs SWAPTER_INIT before it can surface.

Note

Medium Risk
New third-party swap path that builds real deposit spends and orders; behavior is well-tested and mirrors other CEX plugins, but mis-mapped networks or create/fallback edge cases could still misquote or double-order until live with API keys.

Overview
Adds Swapter as a new centralized swap provider so Edge can quote and create orders against api.swapter.io.

The integration registers swapter in the plugin index, adds an Edge→Swapter network mapping (including current identifiers like ARB, POL, AVAX_C), and ships a regenerated swapterMap.json fixture plus partnerJson cases for BTC→ETH/AVAX/USDT mapping checks. CHANGELOG and SWAPTER_INIT (apiKey) are wired for tests/config.

The new plugin loads supported tokens from public /data/coins (no API key on that route), enforces deposit min/max via Swapter’s Edge adapter deposit-range endpoint, then creates orders with type: 'fix' and falls back to float only when fixed create is rejected without an order. It supports from-only quotes, max swaps via getMaxSwappable (fee probe with skipChecks to avoid EVM spend-to-self), maps provider errors to SwapCurrencyError / limit errors, handles memo and TON (GRAM) / BSV (BCHSV) ticker overrides, and includes focused mocha coverage for range checks and create error handling.

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

Comment thread src/swap/central/swapter.ts Outdated
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59a7a92508

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/swap/central/swapter.ts Outdated
Comment thread src/swap/central/swapter.ts Outdated
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from 59a7a92 to 1ad9e59 Compare July 15, 2026 00:19
Comment thread src/swap/central/swapter.ts
Comment thread src/mappings/swapter.ts
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from 1ad9e59 to 2110233 Compare July 15, 2026 00:23
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from 2110233 to 5235c38 Compare July 15, 2026 00:28
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from 5235c38 to 3876435 Compare July 23, 2026 00:19
Comment thread src/swap/central/swapter.ts Outdated
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from 3876435 to 8ed6ef8 Compare July 23, 2026 00:24
@j0ntz

j0ntz commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence — in-app Swapter swap (ETH→LTC) executed, followup with working key bdd7a874

agent proof 1216571782597915 01 swapter quote

agent proof 1216571782597915 01 swapter quote

agent proof 1216571782597915 02 swap success

agent proof 1216571782597915 02 swap success

agent proof 1216571782597915 03 txn details

agent proof 1216571782597915 03 txn details

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

Comment thread src/swap/central/swapter.ts Outdated
Comment thread src/swap/central/swapter.ts Outdated
Comment thread src/swap/central/swapter.ts Outdated
Comment thread src/swap/central/swapter.ts
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch 2 times, most recently from 632629b to d7a85e5 Compare July 28, 2026 23:32
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts Outdated
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from d7a85e5 to 4b409a5 Compare July 28, 2026 23:44
Comment thread src/swap/central/swapter.ts
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from 4b409a5 to d09e981 Compare July 28, 2026 23:54
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts
Comment thread src/swap/central/swapter.ts
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from d09e981 to 441a45e Compare July 29, 2026 00:10
@j0ntz

j0ntz commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence (review-round re-verification, HEAD 441a45e)

swapter quote

swapter quote

swap success

swap success

txn details

txn details

max skipchecks

max skipchecks

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

@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from 441a45e to df0deda Compare August 3, 2026 22:43
@j0ntz

j0ntz commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence

swapter quote

swapter quote

swap success

swap success

swap tx recorded

swap tx recorded

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

@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 2 potential issues.

Fix All in Cursor

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

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit df0deda. Configure here.

Comment thread src/mappings/swapter.ts
Comment thread src/mappings/swapter.ts
Recreates partner PR #455 (author markovo4) on an EdgeApp branch, rebased
onto master, with the integration brought to current plugin conventions:

- Route quotes through getMaxSwappable so max swaps resolve, using a
  quote-only probe order that prices fees against the user's own refund
  address rather than creating an order that would be abandoned.
- Drop the estimate round trip. Its response was fetched and discarded;
  create returns the authoritative amounts and the probe only needs a
  spendInfo to price fees.
- Correct nine chain mappings against the live /data/coins network list.
  Swapter has renamed several networks since the partner branch was cut
  (ARBITRUM to ARB, OPTIMISM to OP, POLYGON to POL, SOLANA to SOL,
  ETHW to 'ETHEREUM POW'), which silently broke those chains. Bitcoin SV,
  Coreum, FIO and Telos are no longer offered in a form Edge can use, so
  they map to null.
- Regenerate the partnerJson fixture from /data/coins so it reflects
  Swapter's real network identifiers and stays a subset of the mapping.

The partner branch also added a self-referential edge-exchange-plugins
dependency and a TypeScript 6 upgrade; both are dropped as unrelated.
@j0ntz
j0ntz force-pushed the jon/swapter-integration branch from df0deda to 70a0912 Compare August 7, 2026 18:04
@j0ntz

j0ntz commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence — LTC to DGB swap on the deposit-range build (2026-08-07)

swapter quote

swapter quote

swap success

swap success

swap tx recorded

swap tx recorded

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

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