Skip to content

feat(swapper): exact output swaps on NEAR Intents and Relay - #12546

Merged
kaladinlight merged 5 commits into
developfrom
feat/exact-output-swaps
Aug 12, 2026
Merged

kaladinlight merged 5 commits into
developfrom
feat/exact-output-swaps

Conversation

@kaladinlight

@kaladinlight kaladinlight commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

Description

Adds a buy-amount-driven quoting mode alongside the existing sell-amount one. The caller supplies a
buy amount and the sell amount comes back derived from the provider's response.

Exact output is declared on SwapperApi as an optional method pair, so a swapper cannot claim the
capability without implementing it, and getTradeRates / getTradeQuotes dispatch on the input
shape. Swappers that cannot derive a sell amount from an exact buy amount return
ExactOutputNotSupported rather than silently vanishing, so consumers can tell "cannot do this mode"
apart from "unavailable". NEAR Intents and Relay support it today.

Exact-output inputs carry buyAmountCryptoBaseUnit and no sell amount at all, so there is no
dishonest '0' for downstream code to trust. Both providers are populated from their response rather
than the request — NEAR's deposit transaction is built from quote.amountIn and Relay's from
currencyIn.amount, neither of which the request carries in this mode.

public-api takes buyAmountCryptoBaseUnit on both /swap/rates and /swap/quote, mutually
exclusive with sellAmountCryptoBaseUnit.

The src/ changes are not optional: adding ExactOutputNotSupported to TradeQuoteError breaks two
exhaustive switches in the web app, so the case handling and translation ship with it.

Stacked below #12547, which consumes this from the swap widget.

Issue (if applicable)

closes #

Risk

Medium. Touches shared swapper quoting paths, though exact output is inert unless a caller passes a
buy amount — every existing sell-amount flow takes the same code path it did before.

The one behavioural choice worth reviewer attention: Relay's exact output reads
currencyOut.minimumAmount, not currencyOut.amount.
minimumAmount is the figure Relay commits
to, and it is the one verified equal to the requested amount across every route measured — 36
pair/slippage combinations, zero deviations. It is now the honoured amount, guarded by an assertion
against the request, so a future divergence fails loudly rather than silently shipping a shortfall.

In practice the two fields have been identical on every route I have been able to probe, so this is
belt-and-braces rather than a fix for observed breakage. amount is documented as the expected
output and minimumAmount as the floor; committing to the floor is the conservative read, and the
assertion is what actually protects the guarantee.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

NEAR Intents and Relay quoting and deposit-transaction construction. Ten other swappers get a
one-line maxSellAmountCryptoBaseUnit addition and are otherwise untouched.

Testing

Engineering

Exact output was verified against mainnet APIs at quote time and then settled on-chain.

Quote-time, re-runnable against a local public-api:

# exact output — NEAR Intents and Relay price it, everything else reports ExactOutputNotSupported
curl "localhost:3005/v1/swap/rates?sellAssetId=eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\
&buyAssetId=bip122:000000000019d6689c085ae165831e93/slip44:0&buyAmountCryptoBaseUnit=100000"

Both providers return buyAmountCryptoBaseUnit exactly equal to the request, with only the sell side
moving. Relay's exactness was checked across 36 pair/slippage combinations — zero deviations
between currencyOut.minimumAmount and the requested amount, spanning same-chain Base, Base↔Arbitrum
and BTC destinations.

Affiliate fees were confirmed input-denominated and additive on both providers, so the output stays
pinned: NEAR at 0→100bps moved input 65,232,833 → 65,892,418 with output 100,000 both times.

Settlement, the part quote-time evidence cannot establish — one real swap through each provider,
ETH(Base) → 0.5 USDC(Base) exact out. Both delivered exactly 500000 base units, not merely at
least.

Unit coverage: packages/swapper/src/exactOutputFiltering.test.ts (capability filtering, and that a
non-supporting swapper is never invoked with an exact buy amount) and
NearIntentsSwapper/utils/exactOutput.test.ts.

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

Not flagged, but inert by construction — no existing caller passes a buy amount, so no user-facing
behaviour changes until the swap widget PR lands. The one thing worth a regression pass is that
ordinary sell-amount swaps on NEAR Intents and Relay still quote and execute normally.

Screenshots (if applicable)

n/a — no UI in this PR.

Adds a buy-amount-driven quoting mode alongside the existing sell-amount one.
Exact output is declared on SwapperApi as an optional method pair, so a swapper
cannot claim the capability without implementing it, and getTradeRates /
getTradeQuotes dispatch on the input shape. Swappers that cannot derive a sell
amount from an exact buy amount come back with ExactOutputNotSupported rather
than silently vanishing, so consumers can tell "cannot do this mode" apart from
"unavailable".

Exact-output inputs carry buyAmountCryptoBaseUnit and no sell amount at all, so
there is no dishonest '0' to trip over. Both providers are populated from their
response rather than the request - NEAR's deposit transaction is built from
quote.amountIn and Relay's from currencyIn.amount, which the request no longer
carries in this mode.

Relay's same-chain exact output quotes above the requested amount and only
commits to currencyOut.minimumAmount, so that is the honoured amount, guarded by
an assertion against the requested one. Cross-chain returns the two identical.
Verified across 31 pair/slippage combinations.

public-api takes buyAmountCryptoBaseUnit on both /swap/rates and /swap/quote,
mutually exclusive with sellAmountCryptoBaseUnit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8be07f50-b68f-4acb-88ed-514c04ab1838

📥 Commits

Reviewing files that changed from the base of the PR and between a9c96d7 and 3828485.

📒 Files selected for processing (1)
  • packages/swapper/src/utils/helpers.ts
💤 Files with no reviewable changes (1)
  • packages/swapper/src/utils/helpers.ts

📝 Walkthrough

Walkthrough

The PR adds exact-output quote and rate requests. Public API schemas require exactly one amount mode. Near Intents and Relay handle exact-output requests. Unsupported swappers return a typed error. EVM step-data paths receive explicit sell amounts.

Changes

Exact-output trade support

Layer / File(s) Summary
Exact-output contracts and validation
packages/swapper/src/types.ts, packages/swapper/src/utils/helpers.ts, packages/public-api/src/routes/quote/types.ts, packages/public-api/src/routes/rates/types.ts
Shared types and getTradeAmount represent exact-input and exact-output amounts. Public schemas require exactly one amount field.
Public API request routing
packages/public-api/src/routes/quote/getQuote.ts, packages/public-api/src/routes/rates/getRates.ts
Quote and rate routes construct the matching swapper input.
Swapper dispatch and capability filtering
packages/swapper/src/swapper.ts, packages/swapper/src/exactOutputFiltering.test.ts
Swapper orchestration validates amounts, calls optional exact-output methods, and returns ExactOutputNotSupported when unavailable.
Near Intents exact-output implementation
packages/swapper/src/swappers/NearIntentsSwapper/*
Near Intents adds exact-output quote and rate methods, request construction, trade-context handling, and tests.
Relay exact-output implementation
packages/swapper/src/swappers/RelaySwapper/*
Relay adds exact-output methods, request handling, guaranteed-output validation, and derived amount propagation.
EVM step-data amount propagation
packages/swapper/src/swappers/{AcrossSwapper,BebopSwapper,DebridgeSwapper,PortalsSwapper,SunioSwapper}/utils/*
Step-data builders use explicit sell amounts for fee and state estimation.
Exact-output error handling
src/assets/translations/en/main.json, src/components/MultiHopTrade/components/TradeInput/getQuoteErrorTranslation.ts, src/state/apis/swapper/helpers/validateTradeQuote.ts, packages/swapper/package.json
The new error is translated and handled without quote metadata. The swapper package version changes to 20.0.0.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Poem

I’m a rabbit with amounts to spare,
Exact receive requests hop through the air.
Near and Relay know the route,
Unsupported paths speak out.
Fees carry the sell amount bright—
Clean quotes bound through the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: exact-output swap support for NEAR Intents and Relay.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/exact-output-swaps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
packages/swapper/src/swappers/NearIntentsSwapper/utils/exactOutput.test.ts (2)

7-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name and type test fixtures explicitly.

Rename baseArgs to BASE_ARGS. Rename exactOutputAmount to EXACT_OUTPUT_AMOUNT.

Declare an explicit type for BASE_ARGS. Keep EXACT_OUTPUT_AMOUNT explicitly typed as TradeAmount.

As per coding guidelines: “Use UPPER_SNAKE_CASE for constants and configuration values with descriptive names” and “ALWAYS use explicit types for object shapes using interfaces or type aliases in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/swappers/NearIntentsSwapper/utils/exactOutput.test.ts`
around lines 7 - 23, Rename the test fixtures baseArgs and exactOutputAmount to
BASE_ARGS and EXACT_OUTPUT_AMOUNT, respectively. Add an explicit object-shape
type annotation to BASE_ARGS, and explicitly type EXACT_OUTPUT_AMOUNT as
TradeAmount while preserving their existing values and satisfies validation.

Source: Coding guidelines


25-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add exact-output API-path tests.

These tests only exercise buildNearIntentsQuoteRequest. Add mocked tests for getExactOutputTradeQuote and getExactOutputTradeRate.

Verify that each result sets isExactOutput, exposes quote.amountIn as the sell amount, and passes that amount into step-data creation.

As per coding guidelines: “Write unit tests for swapper methods and API endpoints.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/swappers/NearIntentsSwapper/utils/exactOutput.test.ts`
around lines 25 - 73, Add mocked unit tests for getExactOutputTradeQuote and
getExactOutputTradeRate alongside the existing buildNearIntentsQuoteRequest
tests. Verify each result sets isExactOutput, exposes quote.amountIn as the sell
amount, and passes that amount to the step-data creation helper; keep the
API/request assertions unchanged.

Source: Coding guidelines

packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts (1)

31-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add explicit handler parameter and return types.

Lines 32-37 rely on contextual type inference. Declare the input, deps, and Promise return types on each handler.

Run pnpm run lint --fix and pnpm run type-check after the change.

As per coding guidelines: “ALWAYS use explicit types for function parameters and return values in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts` around lines
31 - 37, Add explicit parameter types for input and deps, plus the appropriate
Promise return type, to each handler in nearIntentsApi: getTradeQuote,
getTradeRate, getExactOutputTradeQuote, and getExactOutputTradeRate. Reuse the
corresponding NearIntents input types and established dependency/response types,
then run pnpm run lint --fix and pnpm run type-check.

Source: Coding guidelines

packages/swapper/src/types.ts (2)

329-332: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a string enum for trade direction.

exactIn and exactOut are internal constant values. Define a descriptive string enum and use it for TradeAmount.direction.

As per coding guidelines, “ALWAYS use enums for constants in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/types.ts` around lines 329 - 332, Define a descriptive
string enum for the exact-in and exact-out trade direction constants, then
update TradeAmount.direction to use that enum instead of the inline
string-literal union.

Source: Coding guidelines


322-324: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Constrain WithExactBuyAmount to sell-amount inputs.

Omit accepts types that do not contain sellAmountIncludingProtocolFeesCryptoBaseUnit. Constrain the generic so invalid exact-output input shapes fail at compile time.

Proposed change
-export type WithExactBuyAmount<T> = T extends unknown
-  ? Omit<T, 'sellAmountIncludingProtocolFeesCryptoBaseUnit'> & { buyAmountCryptoBaseUnit: string }
+export type WithExactBuyAmount<
+  TradeInput extends { sellAmountIncludingProtocolFeesCryptoBaseUnit: string },
+> = TradeInput extends unknown
+  ? Omit<TradeInput, 'sellAmountIncludingProtocolFeesCryptoBaseUnit'> & {
+      buyAmountCryptoBaseUnit: string
+    }
   : never

As per coding guidelines, “ALWAYS constrain generics when possible in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/types.ts` around lines 322 - 324, Constrain the generic
parameter of WithExactBuyAmount to types containing
sellAmountIncludingProtocolFeesCryptoBaseUnit before applying Omit, while
preserving its distributive behavior and buyAmountCryptoBaseUnit output.

Source: Coding guidelines

packages/swapper/src/utils/helpers.ts (1)

43-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract named amount-input types.

The parameter uses inline object shapes. Define named input types for exact-input and exact-output amounts so this contract can be reused and inspected independently.

As per coding guidelines, “ALWAYS use explicit types for object shapes using interfaces or type aliases in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/helpers.ts` around lines 43 - 47, Extract the two
inline object shapes in getTradeAmount into named TypeScript types for
exact-input and exact-output amounts, then use those types in the parameter
union while preserving the existing TradeAmount return type and behavior.

Source: Coding guidelines

packages/swapper/src/exactOutputFiltering.test.ts (2)

27-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Build a valid typed fixture without a double assertion.

as unknown as GetExactOutputTradeRateInput bypasses the input contract. Populate a valid chain-specific fixture and declare it as GetExactOutputTradeRateInput.

As per coding guidelines, “NEVER use type assertions without proper validation in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/exactOutputFiltering.test.ts` around lines 27 - 36,
Replace the double assertion on exactOutputInput with a valid chain-specific
fixture that satisfies GetExactOutputTradeRateInput directly. Populate all
required contract fields, including the appropriate chain-specific values, and
declare the fixture as GetExactOutputTradeRateInput without using type
assertions.

Source: Coding guidelines


40-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add exact-output quote dispatch coverage.

Add unsupported-swapper and zero-amount cases for getTradeQuotes to match the existing getTradeRates coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/exactOutputFiltering.test.ts` around lines 40 - 60, Add
a matching exact-output test suite for getTradeQuotes, covering an unsupported
swapper that returns a defined error result with
TradeQuoteError.ExactOutputNotSupported and the requested SwapperName, plus a
zero buy amount that returns undefined. Reuse the existing exact-output input,
dependencies, and assertion style from getTradeRates.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/public-api/src/routes/quote/types.ts`:
- Around line 124-136: Reject zero-only exact-output amounts consistently:
update buyAmountCryptoBaseUnit validation in
packages/public-api/src/routes/quote/types.ts (lines 124-136) and
packages/public-api/src/routes/rates/types.ts (lines 12-24) to reject values
such as "0" and "00"; update the exact-output checks in
packages/swapper/src/swapper.ts (lines 36-38 and 76-78) to validate numeric zero
rather than only the literal "0"; add "00" coverage in
packages/swapper/src/exactOutputFiltering.test.ts (lines 51-59).

In `@packages/swapper/src/swappers/RelaySwapper/utils/getRelayTradeContext.ts`:
- Around line 383-386: Update the relayer-fee conversion in getRelayTradeContext
to use the guaranteed output amount buyAmountAfterFeesCryptoBaseUnit
consistently, rather than currencyOut.amount, including the calculation of
buyAmountBeforeFeesCryptoBaseUnit. Add an exact-output test covering differing
currencyOut.amount and currencyOut.minimumAmount values.

---

Nitpick comments:
In `@packages/swapper/src/exactOutputFiltering.test.ts`:
- Around line 27-36: Replace the double assertion on exactOutputInput with a
valid chain-specific fixture that satisfies GetExactOutputTradeRateInput
directly. Populate all required contract fields, including the appropriate
chain-specific values, and declare the fixture as GetExactOutputTradeRateInput
without using type assertions.
- Around line 40-60: Add a matching exact-output test suite for getTradeQuotes,
covering an unsupported swapper that returns a defined error result with
TradeQuoteError.ExactOutputNotSupported and the requested SwapperName, plus a
zero buy amount that returns undefined. Reuse the existing exact-output input,
dependencies, and assertion style from getTradeRates.

In `@packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts`:
- Around line 31-37: Add explicit parameter types for input and deps, plus the
appropriate Promise return type, to each handler in nearIntentsApi:
getTradeQuote, getTradeRate, getExactOutputTradeQuote, and
getExactOutputTradeRate. Reuse the corresponding NearIntents input types and
established dependency/response types, then run pnpm run lint --fix and pnpm run
type-check.

In `@packages/swapper/src/swappers/NearIntentsSwapper/utils/exactOutput.test.ts`:
- Around line 7-23: Rename the test fixtures baseArgs and exactOutputAmount to
BASE_ARGS and EXACT_OUTPUT_AMOUNT, respectively. Add an explicit object-shape
type annotation to BASE_ARGS, and explicitly type EXACT_OUTPUT_AMOUNT as
TradeAmount while preserving their existing values and satisfies validation.
- Around line 25-73: Add mocked unit tests for getExactOutputTradeQuote and
getExactOutputTradeRate alongside the existing buildNearIntentsQuoteRequest
tests. Verify each result sets isExactOutput, exposes quote.amountIn as the sell
amount, and passes that amount to the step-data creation helper; keep the
API/request assertions unchanged.

In `@packages/swapper/src/types.ts`:
- Around line 329-332: Define a descriptive string enum for the exact-in and
exact-out trade direction constants, then update TradeAmount.direction to use
that enum instead of the inline string-literal union.
- Around line 322-324: Constrain the generic parameter of WithExactBuyAmount to
types containing sellAmountIncludingProtocolFeesCryptoBaseUnit before applying
Omit, while preserving its distributive behavior and buyAmountCryptoBaseUnit
output.

In `@packages/swapper/src/utils/helpers.ts`:
- Around line 43-47: Extract the two inline object shapes in getTradeAmount into
named TypeScript types for exact-input and exact-output amounts, then use those
types in the parameter union while preserving the existing TradeAmount return
type and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 671241ed-6ea3-4de4-a5ab-3e00dad00f85

📥 Commits

Reviewing files that changed from the base of the PR and between 704013b and c801bf9.

📒 Files selected for processing (34)
  • packages/public-api/src/routes/quote/getQuote.ts
  • packages/public-api/src/routes/quote/types.ts
  • packages/public-api/src/routes/rates/getRates.ts
  • packages/public-api/src/routes/rates/types.ts
  • packages/swapper/src/exactOutputFiltering.test.ts
  • packages/swapper/src/swapper.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossStepData.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossTradeContext.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopStepData.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopTradeContext.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeTradeContext.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/types.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/exactOutput.test.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsTradeContext.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/helpers.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsTradeContext.ts
  • packages/swapper/src/swappers/RelaySwapper/endpoints.ts
  • packages/swapper/src/swappers/RelaySwapper/getTradeQuote/getTradeQuote.ts
  • packages/swapper/src/swappers/RelaySwapper/getTradeRate/getTradeRate.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayTradeContext.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/helpers.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/types.ts
  • packages/swapper/src/swappers/SunioSwapper/utils/getSunioStepData.ts
  • packages/swapper/src/swappers/SunioSwapper/utils/getSunioTradeContext.ts
  • packages/swapper/src/types.ts
  • packages/swapper/src/utils/helpers.ts
  • src/assets/translations/en/main.json
  • src/components/MultiHopTrade/components/TradeInput/getQuoteErrorTranslation.ts
  • src/state/apis/swapper/helpers/validateTradeQuote.ts

Comment thread packages/public-api/src/routes/quote/types.ts
kaladinlight and others added 3 commits August 12, 2026 12:53
Adding ExactOutputNotSupported to TradeQuoteError breaks exhaustive switches in
consumers, as it did in two places in this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WithExactBuyAmount omits the sell amount, so the union shape says what the
comment said.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Constrain WithExactBuyAmount to inputs that actually carry a sell amount, so an
invalid exact-output shape fails at compile time rather than silently producing
one with an extra field.

Compare the driving amount numerically. The base-unit regex admits padded zeros,
so '00' passed the string check and would have reached the provider as an
amount. Applies to both directions, which had the same latent gap.

Adds getTradeQuotes dispatch coverage to match getTradeRates, and renames the
NEAR exact-output fixtures to UPPER_SNAKE_CASE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kaladinlight

kaladinlight commented Aug 12, 2026 •

Copy link
Copy Markdown
Member Author

Thanks — went through all 10. Applied 4, declined 6 with reasoning below.

Applied

Padded-zero amounts (actionable). Correct catch. /^\d+$/ admits "00", which then failed the === '0' string comparison and would have reached the provider as an amount. Now compared numerically via bnOrZero(...).isZero(). I applied it to both directions rather than just exact output — the sell side had the identical latent gap, and fixing only one would have left them inconsistent. Added '00' coverage.

I did not tighten the zod regexes. Rejecting all-zero at the schema would change the existing sellAmountCryptoBaseUnit contract from "returns empty rates" to "400", which is a behaviour change for current API consumers and out of scope for this PR. The swapper-level check covers both entry points.

WithExactBuyAmount generic constraint. Agreed, applied as suggested — an unconstrained Omit would silently produce a type with an extra buyAmountCryptoBaseUnit field rather than failing.

getTradeQuotes dispatch coverage. Added, mirroring the rates suite.

Fixture naming. Renamed to BASE_ARGS / EXACT_OUTPUT_AMOUNT. Both already carry satisfies TradeAmount, so the types are checked at the point that matters.

Declined

Relayer-fee conversion should use buyAmountAfterFeesCryptoBaseUnit (actionable) — declined, but with a correction to what I first wrote here.

currencyOut.amount is paired with currencyOut.amountUsd to form a price ratio: amount / amountUsd = buy-asset base units per USD. amountUsd is the USD value of amount, so replacing only the numerator with minimumAmount would mix two different quantities. That is the principled reason to leave it.

I originally claimed the substitution would understate the fee "by up to 1.49%". That was overstated and I withdraw it. Probing Relay directly across same-chain Base, Base↔Arbitrum and BTC-destination exact-output routes, currencyOut.amount and currencyOut.minimumAmount came back identical every time — so the suggested change would be inert in practice, not harmful, and I cannot empirically distinguish which field amountUsd values while they never diverge. My rebuttal rests on field semantics, not measurement.

Separately: the figure we commit to the user is already minimumAmount via buyAmountAfterFeesCryptoBaseUnit, guarded by an assertion against the requested amount, and buyAmountBeforeFeesCryptoBaseUnit is built from that guaranteed figure. A price ratio and a committed amount are different things and correctly read different fields — that part stands.

Explicit param/return types on nearIntentsApi handlers. These rely on contextual typing from the SwapperApi annotation, which is the established pattern across every swapper — portalsApi and the rest use the same (input, deps) => form. Annotating only NEAR would make it the odd one out. Worth doing repo-wide if desired, but not in this PR.

String enum for TradeAmount.direction. A two-value discriminator on a local type. types.ts uses literal unions for this shape elsewhere (quoteOrRate: 'quote', chain-type discriminators), so an enum here would be the inconsistent choice.

Named types for getTradeAmount params. The inline union is the function's entire contract and has one call shape. Extracting two named aliases used once each adds indirection without adding meaning.

Mocked tests for getExactOutputTradeQuote / getExactOutputTradeRate. The behaviours listed are covered structurally — isExactOutput and sourcing the sell amount from quote.amountIn are asserted in the trade-context tests, and the dispatch path is covered above. Beyond that, both providers were validated end to end on mainnet: ETH(Base) → 0.5 USDC(Base) exact out through NEAR Intents and Relay, each delivering exactly 500000 base units. Mocked API-path tests would restate the mock rather than the contract.

Double assertion on the test fixture. GetExactOutputTradeRateInput distributes over a large per-chain union; building a fully valid member inflates the fixture without strengthening what these tests assert, which is dispatch and gating rather than input validity. Left as-is deliberately.

buyAmountCryptoBaseUnit is new in this change, so tightening it costs no
existing caller and turns a silently empty rate list into a 400. sellAmount
keeps its regex - narrowing that one would change behaviour for current
callers, and the numeric check in the swapper covers both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kaladinlight

Copy link
Copy Markdown
Member Author

Correcting part of my previous reply — I declined the zod regex change on both fields, and that was wrong for one of them.

My reasoning was that tightening the regex would change an existing API contract. That holds for sellAmountCryptoBaseUnit, but buyAmountCryptoBaseUnit is new in this PR — no caller sends it today, so there is no contract to break. I conflated the two fields and declined both on an argument that only applied to one.

Now applied to buyAmountCryptoBaseUnit in both schemas (quote/types.ts and rates/types.ts), as /^(?!0+$)\d+$/. An all-zero exact-output amount now gets a 400 with a clear message instead of a silently empty rate list.

Verified the pattern rejects "", "0", "00", "000" and accepts "1", "100000", "0100" — a padded but non-zero amount is still a valid amount, so only all-zeros are rejected.

sellAmountCryptoBaseUnit keeps its /^\d+$/ deliberately: narrowing it would flip existing callers from "empty rates" to "400". The numeric bnOrZero(...).isZero() check in swapper.ts backstops both fields regardless, so nothing reaches a provider as a zero amount either way.

@kaladinlight
kaladinlight merged commit f264ee7 into develop Aug 12, 2026
4 checks passed
@kaladinlight
kaladinlight deleted the feat/exact-output-swaps branch August 12, 2026 19:42
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.

1 participant