Add Swapter swap provider - #475
Conversation
There was a problem hiding this comment.
💡 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".
59a7a92 to
1ad9e59
Compare
1ad9e59 to
2110233
Compare
2110233 to
5235c38
Compare
5235c38 to
3876435
Compare
3876435 to
8ed6ef8
Compare
632629b to
d7a85e5
Compare
d7a85e5 to
4b409a5
Compare
4b409a5 to
d09e981
Compare
d09e981 to
441a45e
Compare
📸 Test evidence (review-round re-verification, HEAD 441a45e)swapter quote swap success txn details max skipchecks Captured by the agent's in-app test run (build-and-test). |
441a45e to
df0deda
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ 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.
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.
df0deda to
70a0912
Compare














CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Asana task
Adds the Swapter centralized swap provider.
Recreates partner PR #455 (by @markovo4, from
markovo4/edge-exchange-plugins) on anEdgeApp branch so it can run CI, rebased from its
v2.46.0merge-base onto currentmaster. The plugin design, mapping and test cases are the partner's work; this branchbrings 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/coinslist. Swapter has renamedseveral networks since the partner branch was cut in May, and nine of the 43 non-null
mappings no longer resolve:
arbitrumARBITRUMARBoptimismOPTIMISMOPpolygonPOLYGONPOLsolanaSOLANASOLethereumpowETHWETHEREUM POW(note the space)bitcoinsvBSVBCHSV(mapped viaSPECIAL_MAINNET_CASES)coreumCOREfioFIOtelosTELOSTELOS(relisted since; assetTLOS)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.", whilea token quote throws
SwapCurrencyErrorbecause/data/coinsreturns no ticker set forthe unknown network. Verified by driving the plugin against the live API —
polygonatPOLYGONfailsmin-amountwith HTTP 400, and atPOLreturns HTTP 200. Bitcoin SV needs more than amapping entry:
getChainAndTokenCodesderives the mainnet ticker fromcurrencyInfo.currencyCode(BSV) while Swapter lists both the network and the asset asBCHSV, so it carries aSPECIAL_MAINNET_CASESentry (added in the second review roundbelow).
Max swaps now resolve. Every other central plugin routes its quote through
getMaxSwappable; this one did not, so itsquoteFor !== 'from'guard threwSwapCurrencyErrorfor every max ("send all") swap and Swapter silently vanished from theprovider list. It now follows the
nympattern: a quote-only probe order prices feesagainst 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/estimatewas called and cleaned on everyquote, but its response was only logged —
toNativeAmountcomes from thecreateresponse. The probe needs only a
spendInfoto price fees, andcreatereturns theauthoritative amounts, so neither path needs it.
Regenerated
test/partnerJson/swapterMap.jsonfrom/data/coins. The partner fixtureheld 292 networks, 156 of which are not Swapter networks at all (
ERC20,ARBITRUMONE,AVAXCCHAIN, and a bareA), while omitting 72 real ones —POLwas absent entirely.Every other fixture in
test/partnerJson/is a subset of its provider's mapped codes; thisone 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.3upgrade, a newts-nodedevDependency, andtheir
yarn.lockfallout. This branch touches no dependency files.Also registered
swapteralphabetically insrc/index.tsand dropped the whitespace-onlyedits to
partnerJson.test.ts.Testing
verify-repo.shpasses (prepare, eslint,tsc, mocha — 18 partnerJson cases including 3new 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:0.56602 LTC → 346.92 DOGErendered as Powered bySwapter, 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 thetransaction list recorded
Swap Funds / Exchange:To DOGE.0.77014 LTC → 9,499.5 DGBrendered 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— theFixed Quotelabel confirming thetype: 'fix'path rather than the float fallback.Screenshots are attached in a PR comment.
asSwapterCreateResponsehas therefore been validated against realcreatepayloads./data/coinsno longer rejects a request carrying anX-API-KEY(Swapter fixed that ontheir side), but the key-stripping stays in place as cheap insurance against a revert.
Static checks:
tscclean and 59 mocha cases pass, including the SwapterpartnerJsoncases asserting against the regenerated live fixture.
Review-round changes (addressing @peachbits)
ceilfor the below-limit native minimum (never understate Swapter's floor) andfloorfor the payouttoNativeAmount(never over-promise the receive amount), replacinground.userSettings.swapTypeselection (the GUI never passes it, which pinned the plugin to float). Swapter 500stype: 'fixed'for pairs it cannot fix, so that create error is suppressed and the float fallback runs.SwapterCreateRejectedsentinel), 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: trueon the max-fee probe so EVM max swaps no longer fail withSpendToSelfError(matches the NYM fix).SPECIAL_MAINNET_CASESpins native TON to Swapter'sGRAMcoin (audited all mapped networks; TON was the only native-coin mismatch).eqwould 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, notfixed. An unrecognized value is not rejectedas 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:
Driving the real plugin now issues exactly one
createcall carrying"type":"fix"andtakes 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. Addedwhat live confirms and rejected what it contradicts:
telosTELOSTLOSthorchainruneRUNERUNEhyperevmHYPEREVMHYPEmonadMONADMONbitcoinsvBCHSVBCHSV(viaSPECIAL_MAINNET_CASES)Each was confirmed to quote in both directions through
min-amount, and each resolves tothose exact codes when driven through the real plugin. The partner's own file maps
bitcoinsvtoBSV, which does not exist on/data/coinsand failsmin-amountwithcode 1;coreumstaysnullon both sides because Swapter'sCOREnetwork isambiguous 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-rangeas the source of truth. Both are now done, andthe switch fixes a real user-facing bug rather than being a cosmetic endpoint swap.
/v2/swap/min-amountwas reporting a floor thatcreatedoes not honour. The twodisagree on every pair sampled, and
min-amountis always the lower of the two, so anamount could clear this plugin's own minimum check and then be rejected by
createwith ageneric provider error. Live on ETH→LTC:
create's own rejection floor tracksdeposit-rangeto seven decimals, notmin-amount.The
min-amountcall is gone;deposit-rangereplaces it. The adapter route alsoserializes every numeric field as a string, where the
/v2route returns unquoted JSONnumbers that lose precision in
JSON.parse.A maximum is now enforced.
deposit-rangereturns a ceiling as well as a floor, so anoversized amount raises
SwapAboveLimitErrorwith the real limit instead of creating anorder 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
getMaxSwappableprobe does not enforce the ceiling — it deliberately quotes the wholepre-fee balance, so an above-range balance must clamp through
getMaxSpendablerather thanthrow 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 singlefactory:6into distinct codes, which is what lets the client tell a floor from a ceilingwithout string-matching their English message:
io.swapter.controller.swap.factory:6SwapBelowLimitError(carries the body'smin)io.swapter.controller.swap.factory:9SwapAboveLimitError(carries the body'smax)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
createruns. Both errorsdescribe the amount rather than the swap type, so they propagate directly with no float
retry.
instanceofnever matched an edge-core-js swap error. Its error constructors return aplain
Errorcarrying anameinstead ofthis, soerror instanceof SwapCurrencyErrorwas 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
createon thefloat fallback. Matching on
name(the field core's ownasMaybeSwap*Errorcleaners keyoff) fixes it and covers the two new limit errors as well.
No
estimatecall was added. The same Slack message asks for/adapter/edge/swap/estimateover/v2/swap/estimate; this plugin calls neither, sincecreatereturns the authoritative amounts alongside the deposit address. Adding one wouldbe an extra round trip per quote, and its
deposit.minimumfield reports the stalemin-amountvalue rather than thedeposit-rangefloor.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
asSwapterCreateResponsevalidated against livecreateresponses.mapctlsynchronizer for Swapter./data/coinsneeds no API key, so it is aclean fit, and it would catch renames like
POLYGON→POLautomatically instead ofsilently dropping a chain. Not done here:
mapctl update-mappingsconstructs everyprovider'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.
swapteris registered but is not yet enabled anywhere — the info server serves noSwapter init options, so the GUI needs
SWAPTER_INITbefore 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
swapterin the plugin index, adds an Edge→Swapter network mapping (including current identifiers likeARB,POL,AVAX_C), and ships a regeneratedswapterMap.jsonfixture plus partnerJson cases for BTC→ETH/AVAX/USDT mapping checks. CHANGELOG andSWAPTER_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 adapterdeposit-rangeendpoint, then creates orders withtype: 'fix'and falls back tofloatonly when fixed create is rejected without an order. It supports from-only quotes, max swaps viagetMaxSwappable(fee probe withskipChecksto avoid EVM spend-to-self), maps provider errors toSwapCurrencyError/ 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.