Skip to content

Harden the first-time swap provider implementation - #482

Open
j0ntz wants to merge 4 commits into
masterfrom
jon/harden-provider-impl
Open

Harden the first-time swap provider implementation#482
j0ntz wants to merge 4 commits into
masterfrom
jon/harden-provider-impl

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

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, and
human 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.md tells every new integration to copy
as 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.

Template gap Findings it produced
No getMaxSwappable, so no max-quote model at all #480, #475, #471, #469, #457 (5 of 7)
No checkInvalidTokenIds call #480, #469, #457
denominationToNative results never rounded to integers #475, #462
Untyped catch (error) #481, #475, #471, #457
Provider amounts drive the spend with no bound vs the request #480 (x3, security)
depositExtraId: asOptional(asString) drops numeric memos; "" becomes an empty memo #475 (High, fund-loss class)
isEstimate: false hardcoded #469
Limits compared against the echoed quote amount, not the request #457

The max-quote probe is by far the densest site. getMaxSwappable invokes the
plugin'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, and
clamp 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.ts now models each construct, with the reasoning
inline, since comments are what survive a copy. It models a real two-step
provider (getQuote prices, createOrder commits) so the max probe genuinely
creates nothing, adds fetchProbeOrder + getMaxSwappable with enforceMax and
skipChecks, calls checkInvalidTokenIds, rounds every provider amount to whole
atomic 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), derives isEstimate, and enforces limits
against request.nativeAmount on the side the user pinned.

test/template.test.ts is new. Nothing exercised the template before, since
it is deliberately unregistered. Each case corresponds to a defect that reached
review on a shipped PR.

AGENTS.md and .cursor/BUGBOT.md are new. AGENTS.md indexes the
existing 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.md gains the max-quote section it never
had, 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 verify passes: prepare (incl. tsc), eslint, and mocha. Full suite is
74 passing, 18 of them new. Coverage of src/swap/central/template.ts is 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 at
that point. The ones that still pass cover behavior the old template already had
right (error ranking by priority, building orderUri from a plugin constant, and
the fixed-rate half of the isEstimate pair).

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 surfaced
a 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
asTemplateQuote required orderId and depositAddress. That is fixed at the
root (separate asTemplateQuote / asTemplateOrder, one createOrder call),
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.js is byte-for-byte identical between this branch
and master (1aa17692cf...), because template.ts is unregistered and the rest
of the branch is docs and tests. An in-app drive would execute master's bytes.

@j0ntz
j0ntz force-pushed the jon/harden-provider-impl branch from 5f33264 to 07f5ce4 Compare August 6, 2026 18:30
@j0ntz
j0ntz marked this pull request as ready for review August 6, 2026 18:43
@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.

@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 07f5ce4. Configure here.

Comment thread src/swap/central/template.ts
j0ntz added 4 commits August 6, 2026 11:51
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
j0ntz force-pushed the jon/harden-provider-impl branch from 07f5ce4 to 5a02fb0 Compare August 6, 2026 18:52
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