Harden the first-time swap provider implementation - #482
Open
j0ntz wants to merge 4 commits into
Open
Conversation
j0ntz
force-pushed
the
jon/harden-provider-impl
branch
from
August 6, 2026 18:30
5f33264 to
07f5ce4
Compare
j0ntz
marked this pull request as ready for review
August 6, 2026 18:43
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 07f5ce4. Configure here.
The template is what docs/CREATING_AN_EXCHANGE_PLUGIN.md tells every new provider integration to copy, so each construct it omits gets rediscovered in review on every new plugin. Model the constructs that recur. Add the max-quote probe path. getMaxSwappable rewrites a max request into a from quote for the full pre-fee balance, which is the densest defect site in the review record: probes that create a live order, probes rejected with SpendToSelfError for lacking skipChecks, and probes that throw above-limit on a raw balance instead of clamping. Split quote fetching from order creation, add fetchProbeOrder, and gate the above-limit throw on enforceMax. Call checkInvalidTokenIds, which carries both the repo-wide blocked list and the same-asset guard. Round every provider amount to whole atomic units, up for a floor and down for a ceiling, so no rounding widens the accepted range. Bound the provider-returned source amount by the requested amount before it becomes a signed spend. Clean the deposit memo with asOptionalBlank(asNumberString), so a numeric destination tag or an empty string cannot silently become an untagged deposit. Derive isEstimate from the quote instead of hardcoding false, enforce limits against the requested amount rather than the echoed one, and type the catch binding.
The template is never registered, so nothing exercised it. Each construct in it is a claim about what a correct plugin does, and these tests are what make those claims checkable. Every case corresponds to a defect that reached review on a shipped provider PR: the max probe spending to self, the probe creating an order, the probe throwing above-limit on a raw balance, limits compared against the echoed amount, a floor limit rounding down, a provider amount exceeding the request, a numeric or blank deposit memo, a same-asset swap reaching the network, and a floating rate reported as guaranteed. The suite discriminates: 13 of its 16 cases fail against the previous template. The three that pass cover behavior it already had right. It also gives a new integration a starting test suite to copy alongside the template, since the fake wallet models the parts that matter here, the SpendToSelfError guard and the fee-trimming getMaxSpendable.
AGENTS.md indexes the existing docs and carries the two facts an agent cannot cheaply discover: swap plugins execute inside edge-core-js's plugin WebView where Metro's debugger cannot reach them, and Edge amounts are integer atomic units while provider APIs speak decimals. .cursor/BUGBOT.md records the conventions that recent provider reviews settled, so each one is cited rather than rediscovered. It also records the two areas where automated review is least reliable here: chain identity claims, which need live provider metadata rather than a numeric id, and the error-body logging the template prescribes.
Add the max-quote section the guide never had. getMaxSwappable runs the quote function twice and hands it the raw pre-fee balance, which is not discoverable from the interface and is where new plugins break most often. Expand amount conversion to cover integer rounding and its direction, and error handling to cover error ranking, enforcing limits against the requested amount, and the two directions a pair failure can be misclassified. Document the fields in EdgeSpendInfo that are decisions rather than boilerplate, and add the guard bounding provider amounts before they become a signed spend. Explain why memo cleaning is a funds-safety choice on memo-based chains. Replace the generic testing and pitfalls lists with the swap paths that actually break and a pre-PR checklist, each item traceable to a finding on a shipped integration.
j0ntz
force-pushed
the
jon/harden-provider-impl
branch
from
August 6, 2026 18:52
07f5ce4 to
5a02fb0
Compare
peachbits
approved these changes
Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Asana: https://app.asana.com/0/1215088146871429/1217131617432935
Collects the review feedback from recent swap provider integrations, finds
what actually recurs, and fixes it at the source.
Evidence base: 80 review threads across the 8 most recent provider PRs
(#481 SimpleSwap,
#480 swaps.xyz,
#475 Swapter,
#471 Changelly,
#469 +
#462 HoudiniSwap,
#457 NYM,
#464 Xgram), from
cursor[bot], the Cursor Security Reviewer,chatgpt-codex-connector, andhuman reviewers.
What the record shows
Nearly every recurring finding traces back to one file:
src/swap/central/template.ts,which
docs/CREATING_AN_EXCHANGE_PLUGIN.mdtells every new integration to copyas its base. The template omits or mis-models the exact constructs reviewers then
ask for, so each new plugin inherits the gap and rediscovers the finding.
getMaxSwappable, so no max-quote model at allcheckInvalidTokenIdscalldenominationToNativeresults never rounded to integerscatch (error)depositExtraId: asOptional(asString)drops numeric memos;""becomes an empty memoisEstimate: falsehardcodedThe max-quote probe is by far the densest site.
getMaxSwappableinvokes theplugin's quote function with the raw pre-fee balance, which is not discoverable
from the interface, so three separate PRs hand-rolled the same three fixes
independently: don't create an order in the probe, set
skipChecks: true, andclamp rather than throw above-limit.
Worth noting the template is already treated as the contract. On #475 a security
finding about logging was answered by citing what the template prescribes.
Changes
src/swap/central/template.tsnow models each construct, with the reasoninginline, since comments are what survive a copy. It models a real two-step
provider (
getQuoteprices,createOrdercommits) so the max probe genuinelycreates nothing, adds
fetchProbeOrder+getMaxSwappablewithenforceMaxandskipChecks, callscheckInvalidTokenIds, rounds every provider amount to wholeatomic units (up for a floor, down for a ceiling), bounds the provider's source
amount by the requested amount before it becomes a signed spend, cleans memos
with
asOptionalBlank(asNumberString), derivesisEstimate, and enforces limitsagainst
request.nativeAmounton the side the user pinned.test/template.test.tsis new. Nothing exercised the template before, sinceit is deliberately unregistered. Each case corresponds to a defect that reached
review on a shipped PR.
AGENTS.mdand.cursor/BUGBOT.mdare new. AGENTS.md indexes theexisting docs plus the two facts that are not cheaply discoverable: plugins run
inside edge-core-js's WebView where Metro's debugger cannot reach, and Edge
amounts are integer atomic units while provider APIs speak decimals. BUGBOT.md
records the conventions these reviews settled, including the two areas where
automated review has been least reliable here (chain identity claims, and the
template's prescribed error-body logging).
docs/CREATING_AN_EXCHANGE_PLUGIN.mdgains the max-quote section it neverhad, deeper amount and error-handling guidance, and closes with a pre-PR
checklist where every item is traceable to a finding on a shipped integration.
Scope note
getMaxSwappable's signature is unchanged and no shipped plugin is touched.Giving the framework a first-class probe signal would be the better long-term
fix, but it is a migration across 12 live providers, so it is recorded as a
follow-up rather than bundled here.
Testing
npm run verifypasses: prepare (incl.tsc), eslint, and mocha. Full suite is74 passing, 18 of them new. Coverage of
src/swap/central/template.tsis 89%statements, 84% branch.
The new suite was checked for discriminating power rather than just green:
swapping
master's template back in fails 13 of the 16 cases that existed atthat point. The ones that still pass cover behavior the old template already had
right (error ranking by priority, building
orderUrifrom a plugin constant, andthe fixed-rate half of the
isEstimatepair).That check kept earning its keep. It caught two of my own tests asserting nothing
useful (the memo cases only checked
pluginId), and reviewing the diff surfaceda bug I had introduced where the limit-error branch read the amount field by
below-vs-above instead of by the pinned side. Bugbot then caught the biggest one:
the template claimed a quote/order split it did not have, because
asTemplateQuoterequiredorderIdanddepositAddress. That is fixed at theroot (separate
asTemplateQuote/asTemplateOrder, onecreateOrdercall),and the probe test now asserts the order endpoint was hit exactly once rather
than counting total requests.
No app-level testing applies, and that is provable rather than assumed: the built
dist/edge-exchange-plugins.jsis byte-for-byte identical between this branchand
master(1aa17692cf...), becausetemplate.tsis unregistered and the restof the branch is docs and tests. An in-app drive would execute
master's bytes.