Skip to content

fix(public-api): reject sell assets we cannot build a transaction for - #12586

Merged
kaladinlight merged 9 commits into
developfrom
chore/api-executable-sell-chains
Aug 13, 2026
Merged

fix(public-api): reject sell assets we cannot build a transaction for#12586
kaladinlight merged 9 commits into
developfrom
chore/api-executable-sell-chains

Conversation

@kaladinlight

Copy link
Copy Markdown
Member

Description

extractTransactionData only serializes four chain namespaces — eip155, bip122, cosmos, solana. Anything else falls off the end and returns undefined, and transformQuoteStep assigns it without complaint. So Tron, Sui, TON, NEAR and Starknet were advertised on /v1/chains and /v1/assets, served rates and quotes, and handed back a quote with nothing to sign.

This stops the API advertising what it can't fulfil:

  • EXECUTABLE_SELL_CHAIN_IDS is derived from SUPPORTED_CHAIN_IDS by namespace, so it can't drift from what the extractor actually handles. Comments on both sides point at each other.
  • /v1/swap/rates and /v1/swap/quote reject those chains as a sell asset with 400 / code: UNSUPPORTED_SELL_CHAIN (distinct from the existing UNSUPPORTED_CHAIN raised for EVM chains with no viem client).
  • They stay valid as a buy asset — buying into Tron only needs a receive address, and the user signs on the sell chain. ETH→TRX and friends are unaffected.
  • /v1/chains gains isSellSupported: boolean so clients can build a correct sell picker. The partner guide documents it, including how to filter assets by joining on chainId (the constraint is chain-level, so there's no per-asset flag).
  • CoW Swap is dropped from ENABLED_SWAPPER_NAMES — it signs an off-chain EIP-712 order rather than a transaction, so every CoW quote carried no transactionData at all. Same defect, at the swapper level. Tracked in SS-5734.
  • getQuote now asserts the step actually has transactionData and returns 502 if not. The namespace gate only proves a chain is serializable; this proves the swapper produced something. Nothing currently reaches it — it's regression prevention.

Two of the six commits are a feature and its revert (allowNonExecutableSellChain, an opt-in to serve rates anyway). It was built for the swap widget's redirect fallback and then backed out: for these five chains the API enables a strictly thinner swapper set than the app does — STON.fi, AVNU, Sun.io and Cetus are the chain-native DEXes and none are API-enabled — so any rate served under the flag would systematically understate what the user receives and argue against clicking through. The revert commit records that reasoning.

Issue (if applicable)

closes #

Risk

Medium, and worth splitting in two because the halves are not equivalent:

  • /v1/swap/quote — fixing a defect, not breaking a contract. These chains returned a 200 whose transactionData was undefined. No consumer could ever have completed a swap that way, so no working integration breaks; the 400 replaces a response that was already a dead end.
  • /v1/swap/rates — a genuine, narrow removal. A rate is useful without transactionData for price display or comparison. Anyone using the API purely to show Tron/NEAR prices loses that.

Separately, dropping CoW Swap removes real rate coverage on Ethereum, Gnosis and Base until SS-5734 lands. That's the change most likely to prompt a "why did rates get worse" question.

No on-chain transaction construction is altered. Executable paths are untouched — the same tx data goes out for every chain that could already be executed.

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

CoW Swap becomes unavailable via the API. Tron, Sui, TON, NEAR and Starknet become unavailable as sell assets (they were never executable). Everything else is unchanged.

Testing

Engineering

Verified end-to-end against a locally built server hitting real mainnet unchained/node/swapper endpoints, at branch tip 9110e0dbf1:

Check Result
Tron/Sui/TON/NEAR/Starknet as sell (rates) 400 UNSUPPORTED_SELL_CHAIN — all five
Tron as sell (quote) 400
USDC → TRX (buy side) 200, 5 valid rates (NEAR Intents, THORChain, Relay, Chainflip, ButterSwap)
ETH → USDC rates 200, 9 swappers, CoW absent
CoW quote 400 Swapper not available: CoW Swap
ETH → USDC quote via 0x 200, transactionData.type: evm
CACAO → BTC via MAYAChain 200, transactionData.type: cosmossdk_msg_deposit
/v1/chains 27 chains, exactly 5 with isSellSupported: false
pnpm --filter @shapeshiftoss/public-api build
node --env-file=<env> packages/public-api/dist/server.cjs
API_URL=http://localhost:3001 vitest run --project public-api-integration   # 44/44 passed
tsc --noEmit -p packages/public-api/tsconfig.json                            # exit 0
eslint packages/public-api/**/*.ts                                           # clean

Integration tests were restructured: a NON_SELLABLE table asserts rejection on both endpoints, that buy-side rates still work for the same chains, and that /v1/chains flags them correctly. A new case asserts every quote in the suite carries transactionData.

Note the full monorepo pnpm type-check could not be run in this worktree (no local node_modules; tsc --build fails in unchained-client codegen and hdwallet-core ethers types before reaching this code — both environmental and unrelated). The standalone public-api check above is clean.

Operations

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

Not user-facing in the web app — the app doesn't consume the public API. For API partners: selecting Tron/Sui/TON/NEAR/Starknet as a sell asset now returns a clear 400 instead of an unusable quote; the same chains as a buy asset are unchanged and should be regression-checked.

Screenshots (if applicable)

N/A

🤖 Generated with Claude Code

kaladinlight and others added 6 commits August 13, 2026 11:46
extractTransactionData only serializes eip155, bip122, cosmos and solana tx
data, so Tron, Sui, TON, NEAR and Starknet quotes came back with no
transactionData at all - advertised as supported, impossible to execute.

Gate rates and quotes on the sell chain being one we can serialize. Those
chains stay valid as a buy asset, where only a receive address is needed, and
/chains now carries isSellSupported so clients can build a correct sell picker.

Also drop CoW Swap from the enabled swappers: it signs an off-chain EIP-712
order rather than a transaction, which the wire has no shape for yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rates are informational, so a client that fulfils the swap elsewhere - the
widget hands these off to app.shapeshift.com - can ask for them anyway via
allowNonExecutableSellChain. Defaults off, so a consumer with no fallback path
still gets nothing it cannot execute.

/v1/swap/quote stays hard gated with no escape hatch, since a quote implies
something to sign.

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

Its only intended consumer was the widget's pre-redirect rate estimate, which
is being dropped: the api enables a thinner swapper set than the app for the
chains in question, so any rate served under the flag would understate what the
user receives. Nothing else asks for rates it cannot execute, so the escape
hatch has no caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing imports it - the set and the predicate are what callers use.

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

Review follow-ups. The rejection carried no code, so clients had to regex the
message to tell it from a validation 400 - it now sends UNSUPPORTED_SELL_CHAIN,
distinct from the existing UNSUPPORTED_CHAIN raised for evm chains with no viem
client.

The namespace gate only proves a chain is serializable, not that this swapper
produced anything; assert transactionData on the step so an enabled swapper that
returns nothing to sign fails loudly instead of 200ing an unusable quote.

Document destination-only chains in the partner guide, which described neither
isSellSupported nor the new 400.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kaladinlight
kaladinlight requested a review from a team as a code owner August 13, 2026 19:39
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kaladinlight, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 999469dd-1e9d-43df-9330-fe86c87fa784

📥 Commits

Reviewing files that changed from the base of the PR and between 7904d09 and 5662d12.

📒 Files selected for processing (7)
  • packages/public-api/docs/rest-api-guide.md
  • packages/public-api/src/constants.ts
  • packages/public-api/src/integration.test.ts
  • packages/public-api/src/routes/chains/types.ts
  • packages/public-api/src/routes/chains/utils.ts
  • packages/public-api/src/routes/quote/getQuote.ts
  • packages/public-api/src/routes/rates/getRates.ts
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/api-executable-sell-chains

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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kaladinlight
kaladinlight force-pushed the chore/api-executable-sell-chains branch from e911bc6 to a29acb2 Compare August 13, 2026 19:42
@kaladinlight
kaladinlight enabled auto-merge (squash) August 13, 2026 19:47
Caught by the root type-check, which runs over packages/**/* with options the
standalone public-api tsconfig does not share.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kaladinlight
kaladinlight merged commit 52b1a6a into develop Aug 13, 2026
4 checks passed
@kaladinlight
kaladinlight deleted the chore/api-executable-sell-chains branch August 13, 2026 20:05
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