Repository navigation
feat(swapper): exact output swaps on NEAR Intents and Relay - #12546
Conversation
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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughThe 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. ChangesExact-output trade support
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (8)
packages/swapper/src/swappers/NearIntentsSwapper/utils/exactOutput.test.ts (2)
7-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueName and type test fixtures explicitly.
Rename
baseArgstoBASE_ARGS. RenameexactOutputAmounttoEXACT_OUTPUT_AMOUNT.Declare an explicit type for
BASE_ARGS. KeepEXACT_OUTPUT_AMOUNTexplicitly typed asTradeAmount.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 liftAdd exact-output API-path tests.
These tests only exercise
buildNearIntentsQuoteRequest. Add mocked tests forgetExactOutputTradeQuoteandgetExactOutputTradeRate.Verify that each result sets
isExactOutput, exposesquote.amountInas 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 valueAdd 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 --fixandpnpm run type-checkafter 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 valueUse a string enum for trade direction.
exactInandexactOutare internal constant values. Define a descriptive string enum and use it forTradeAmount.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 winConstrain
WithExactBuyAmountto sell-amount inputs.
Omitaccepts types that do not containsellAmountIncludingProtocolFeesCryptoBaseUnit. 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 + } : neverAs 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 valueExtract 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 winBuild a valid typed fixture without a double assertion.
as unknown as GetExactOutputTradeRateInputbypasses the input contract. Populate a valid chain-specific fixture and declare it asGetExactOutputTradeRateInput.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 winAdd exact-output quote dispatch coverage.
Add unsupported-swapper and zero-amount cases for
getTradeQuotesto match the existinggetTradeRatescoverage.🤖 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
📒 Files selected for processing (34)
packages/public-api/src/routes/quote/getQuote.tspackages/public-api/src/routes/quote/types.tspackages/public-api/src/routes/rates/getRates.tspackages/public-api/src/routes/rates/types.tspackages/swapper/src/exactOutputFiltering.test.tspackages/swapper/src/swapper.tspackages/swapper/src/swappers/AcrossSwapper/utils/getAcrossStepData.tspackages/swapper/src/swappers/AcrossSwapper/utils/getAcrossTradeContext.tspackages/swapper/src/swappers/BebopSwapper/utils/getBebopStepData.tspackages/swapper/src/swappers/BebopSwapper/utils/getBebopTradeContext.tspackages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.tspackages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeTradeContext.tspackages/swapper/src/swappers/NearIntentsSwapper/endpoints.tspackages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.tspackages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.tspackages/swapper/src/swappers/NearIntentsSwapper/types.tspackages/swapper/src/swappers/NearIntentsSwapper/utils/exactOutput.test.tspackages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsTradeContext.tspackages/swapper/src/swappers/NearIntentsSwapper/utils/helpers.tspackages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.tspackages/swapper/src/swappers/PortalsSwapper/utils/getPortalsTradeContext.tspackages/swapper/src/swappers/RelaySwapper/endpoints.tspackages/swapper/src/swappers/RelaySwapper/getTradeQuote/getTradeQuote.tspackages/swapper/src/swappers/RelaySwapper/getTradeRate/getTradeRate.tspackages/swapper/src/swappers/RelaySwapper/utils/getRelayTradeContext.tspackages/swapper/src/swappers/RelaySwapper/utils/helpers.tspackages/swapper/src/swappers/RelaySwapper/utils/types.tspackages/swapper/src/swappers/SunioSwapper/utils/getSunioStepData.tspackages/swapper/src/swappers/SunioSwapper/utils/getSunioTradeContext.tspackages/swapper/src/types.tspackages/swapper/src/utils/helpers.tssrc/assets/translations/en/main.jsonsrc/components/MultiHopTrade/components/TradeInput/getQuoteErrorTranslation.tssrc/state/apis/swapper/helpers/validateTradeQuote.ts
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>
|
Thanks — went through all 10. Applied 4, declined 6 with reasoning below. AppliedPadded-zero amounts (actionable). Correct catch. I did not tighten the zod regexes. Rejecting all-zero at the schema would change the existing
Fixture naming. Renamed to DeclinedRelayer-fee conversion should use
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, Separately: the figure we commit to the user is already Explicit param/return types on String enum for Named types for Mocked tests for Double assertion on the test fixture. |
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>
|
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 Now applied to Verified the pattern rejects
|
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
SwapperApias an optional method pair, so a swapper cannot claim thecapability without implementing it, and
getTradeRates/getTradeQuotesdispatch on the inputshape. Swappers that cannot derive a sell amount from an exact buy amount return
ExactOutputNotSupportedrather 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
buyAmountCryptoBaseUnitand no sell amount at all, so there is nodishonest
'0'for downstream code to trust. Both providers are populated from their response ratherthan the request — NEAR's deposit transaction is built from
quote.amountInand Relay's fromcurrencyIn.amount, neither of which the request carries in this mode.public-apitakesbuyAmountCryptoBaseUniton both/swap/ratesand/swap/quote, mutuallyexclusive with
sellAmountCryptoBaseUnit.The
src/changes are not optional: addingExactOutputNotSupportedtoTradeQuoteErrorbreaks twoexhaustive 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, notcurrencyOut.amount.minimumAmountis the figure Relay commitsto, 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.
amountis documented as the expectedoutput and
minimumAmountas the floor; committing to the floor is the conservative read, and theassertion is what actually protects the guarantee.
NEAR Intents and Relay quoting and deposit-transaction construction. Ten other swappers get a
one-line
maxSellAmountCryptoBaseUnitaddition 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:Both providers return
buyAmountCryptoBaseUnitexactly equal to the request, with only the sell sidemoving. Relay's exactness was checked across 36 pair/slippage combinations — zero deviations
between
currencyOut.minimumAmountand the requested amount, spanning same-chain Base, Base↔Arbitrumand 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 atleast.
Unit coverage:
packages/swapper/src/exactOutputFiltering.test.ts(capability filtering, and that anon-supporting swapper is never invoked with an exact buy amount) and
NearIntentsSwapper/utils/exactOutput.test.ts.Operations
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.