Skip to content

Add Swapter swap provider - #475

Open
j0ntz wants to merge 2 commits into
masterfrom
jon/swapter-integration
Open

Add Swapter swap provider#475
j0ntz wants to merge 2 commits 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 (none — see below)
coreum CORE (none)
fio FIO (none)
telos TELOS (only TELOS(EVM); Edge's telos is EOSIO)

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 is
dropped rather than remapped to Swapter's BCHSV: getChainAndTokenCodes derives the
mainnet ticker from currencyInfo.currencyCode (BSV), so that pair needs a
SPECIAL_MAINNET_CASES entry to work at all.

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 executed in-app swap is not part of this verification, and the plugin is unproven past
order creation.
The throwaway key on the Asana task is not authorized: /v2/swap/create
and /v2/swap/estimate return HTTP 401 under every documented auth variant, and
/data/coins 401s whenever an X-API-KEY header is present at all. (/v2/swap/min-amount
is unauthenticated, which is what made the mapping verification above possible.)

Driving the real plugin against the live API with that key gives:

GET  /data/coins            -> HTTP 401   (warn + empty ticker map, as designed)
POST /v2/swap/min-amount    -> HTTP 200   {"amount":"0.00116310"}
POST /v2/swap/create        -> HTTP 401   {"info":{"type":"float","refundAddress":"bc1q…"},
                                           "deposit":{"coin":"BTC","network":"BTC","amount":0.1},
                                           "withdraw":{"coin":"ETH","network":"ETH","address":"0x71C7…"}}
=> Error: Swapter returned error code 401

So request construction and the mapping are confirmed end-to-end, and auth is the only
thing between this and an executed swap. A valid Swapter partner key is needed to
sign this off
; until then the create response cleaner (asSwapterCreateResponse) has
never seen a real payload and is trusted from the published docs alone.

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.

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 creates real deposit orders and builds spends from API responses; behavior aligns with other CEX plugins but live create payloads were not verified with an authorized key, and stale network mappings can fail silently at quote time.

Overview
Adds Swapter as a new centralized swap provider: plugin registration, Edge→Swapter chain mapping, and partner JSON tests backed by a regenerated /data/coins fixture.

Quoting follows the same central-plugin pattern as peers: getMaxSwappable with a fee probe (user refund address, skipChecks so EVM max swaps don’t hit spend-to-self), then /v2/swap/min-amount and /v2/swap/create for a single real order—no separate estimate call. It tries fixed rate first and falls back to float only when create is rejected without an order; unsupported pairs map to SwapCurrencyError. Supported assets are refreshed hourly from public /data/coins (no API key on that route). Network codes were corrected against the live API (e.g. POL, ARB, AVAX_C, ETHEREUM POW); native TON uses SPECIAL_MAINNET_CASES (GRAM on Swapter). Only from / max (rewritten to from) quotes are supported.

SWAPTER_INIT is added for tests; the plugin is registered but not enabled in production until init options are served.

Reviewed by Cursor Bugbot for commit 441a45e. 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
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 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

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

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 d09e981. Configure here.

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).

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