Skip to content

fix(solana): native SOL by mint, balances keys and errors, Meteora add/open fixes, wallet activity route - #724

Closed
fengtality wants to merge 8 commits into
developmentfrom
fix/solana-fork-fixes
Closed

fengtality wants to merge 8 commits into
developmentfrom
fix/solana-fork-fixes

Conversation

@fengtality

@fengtality fengtality commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Small Solana fixes found while running Gateway forks against mainnet, ported to development. One commit per fix, each with a test that fails without it (except the docs commit). Review feedback from Greptile is folded into the commits; fix 8 was added for its first finding.

Fixes

  1. Native SOL by mint (eaaff1c18; source d8c4870a). The Meteora DLMM open, add, remove and close routes, Raydium CLMM add, and Solana.extractClmmBalanceChanges treated a token as native SOL when its symbol was 'SOL'. Any mint's metadata can claim that symbol. They now compare the mint with NATIVE_MINT.
  2. Balances error pass-through (c59f3ed24; source d8c4870a). POST /chains/solana/balances turned every error except 429 into a 500. A 4xx now passes through. Added here: getBalances answers 400 for a malformed wallet address, which used to be a 500.
  3. Meteora add-liquidity checks balances by mint (d6a41a004, completed by fix 8; source d8c4870a). development has the same fault: the route asked getBalances for symbols, and a token outside the token list (named DUMMY_… or UNKNOWN) read as 0, so a funded wallet got "Insufficient balance". It now asks by the pool's mints. Changed from the source: it reads each side under the key getBalances reports (the list symbol for a listed mint, the mint otherwise, SOL for native) and throws a clear error if that key is missing. This depends on fix 4, and on fix 8 for unlisted tokens.
  4. Balances by mint report the symbol (1bf6e95b1; source f82e207e). A listed token asked for by its mint is reported under its symbol, the same as when asked for by symbol. Added here: the shortcut for a wallet with no token accounts is removed, so it goes through the same steps and the keys no longer depend on whether the wallet holds the token. A token-list entry of another mint with the symbol SOL is refused with a clear error, because its balance would overwrite the native SOL key.
  5. Empty DLMM position cleanup (9b5a5712c; source b7684ba6, openPosition.ts part only). When the first deposit chunk of a wide Meteora open fails, the route reads the position. If it holds nothing, the route closes it with closePositionIfEmpty and returns its rent. Added here: a send can fail after its chunk was broadcast and still land, so a position that holds tokens is left open and the error says what it holds. If the read or the close fails, the error names the position.
  6. Wallet activity route (a496d80b8; source 67677eb5). GET /chains/:chain/transactions lists a wallet's recent Solana transactions, each named as what it did (swap, open_position, add_liquidity, remove_liquidity, collect_fees, close_position, deposit, withdraw), with the wallet's own balance changes. Ethereum answers 501. Changed from the source: covers only venues development has connectors and SDKs for (Meteora DLMM and DAMM v2, Raydium CLMM, CPMM and AMM v4, Orca, PancakeSwap Solana CLMM, Jupiter). The launchpad venues are left for feat: launchpad connectors (pump.fun, Meteora DBC, Raydium LaunchLab) #721. A malformed address answers 400 and other 4xx pass through. The venue tables hold mainnet program ids, so another cluster answers 400 instead of misreading every venue call as a transfer. With no venue, the tokens that moved decide deposit or withdraw (the SOL a new token account costs is not a withdrawal), a wrap or unwrap is neither, and balances are read per side with that side's owner, so an account handed to another owner counts. It reads the wallet address's own signature history, so a transfer into an existing token account that does not name the wallet is not listed; the route description says so. Adds an operationId and regenerates openapi.json.
  7. Docs (afd1ffdec; source 738b1821, 11f8ba5c). docs/cross-venue-pool-normalisation.md: base/quote orientation differs by venue, and Orca's tradeEnableTimestamp is always the epoch. Changed from the source: three corrections. The source said a pool with SOL on the unexpected side cannot take a quote-denominated deposit; it can, with the amount in the other field. The source said a minimum-age filter excludes all of Orca; an epoch timestamp makes every pool look old, so it passes them all. The source said Gateway surfaces no yield fields, but on development Orca pool-info returns yieldOverTvl and fetch-pools returns apr.
  8. Balances for an unlisted mint (6786f5969; source: the generic part of a later fork commit). fetchTokenAccounts dropped every token account whose mint is not in the token list, so getBalances reported 0 for an unlisted mint the wallet holds, and fix 3 still refused a funded wallet for one. Those accounts are now kept, and the balance uses the decimals read from the mint through the token program that owns the account. A mint that cannot be read is now an error, instead of a balance computed with a guessed 9 decimals after a 1 s timeout. A rate limit on the mint read stays a 429. The default list-only view is unchanged. The fix 3 test now runs the real fetchTokenAccounts over encoded token accounts.

Change to the signing-gate spec test

test/spec/signing-routes-are-gated.test.ts treats any operation whose response has a signature it was not given as a signing route that must be gated. The new GET route lists past signatures and signs nothing, so the test now puts GET operations in a historyRoutes list. That list is asserted exactly (['/chains/ethereum/transactions']), so any new GET that returns a signature still fails the test until someone looks at it. This is the same shape #721 uses for its launch-history route, so the two only conflict on the contents of that list.

Overlap

Checked against #721 and #723. Neither has these changes. #721 touches chain.routes.ts, chain-schema.ts, operation-ids.ts and the same spec test, so expect small textual conflicts there.

Tests

pnpm build, pnpm lint (0 errors) and GATEWAY_TEST_MODE=dev pnpm test with a fresh conf/ copied from src/templates: 177 suites, 1540 tests passed. pnpm generate:openapi leaves openapi.json unchanged.

🤖 Generated with Claude Code

The Meteora DLMM open, add, remove and close routes, Raydium CLMM add, and
Solana.extractClmmBalanceChanges treated a token as native SOL when its
symbol was 'SOL'. Any mint's metadata can claim that symbol. Such a token
was then measured on the native balance, or had a fee or rent correction
applied to it. Compare the mint with NATIVE_MINT instead.
/chains/solana/balances turned every error except a 429 into a 500. A 4xx
now passes through as it is. getBalances answers 400 for a malformed wallet
address, which used to surface as a 500.
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the numbered balance-key finding is fixed, and no new blocking issue was established.

Summary

This PR switches native SOL checks to mint addresses, makes requested balance keys consistent, supports unlisted mints, and adds cleanup for empty Meteora positions.

  • Solana liquidity routes identify native SOL by its mint.
  • Balance lookups find listed and unlisted Solana tokens by mint.
  • Failed Meteora opens clean up empty positions but keep funded ones open.
  • Solana wallets can page through named transaction activity.
  • Pool notes explain venue token order and Orca's missing age data.

Review checks were static; no tests were run during this review.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Wallet transactions request] --> B{Solana mainnet?}
  B -->|No| C[Reject unsupported chain or network]
  B -->|Yes| D[Read wallet signature page]
  D --> E[Read transactions five at a time]
  E --> F[Identify venue actions]
  E --> G[Calculate wallet balance changes]
  F --> H[Choose activity kind]
  G --> H
  H --> I[Return entries and page cursor]
Loading

Reviews (4) · Last reviewed commit: "fix(solana): getBalances reads an unlist..." · Reviewed by Greptile

Comment thread src/connectors/meteora/clmm-routes/addLiquidity.ts
Comment thread src/chains/solana/solana-activity.ts
Comment thread src/chains/solana/solana-activity.ts
Comment thread src/chains/solana/solana-activity.ts Outdated
Comment thread src/connectors/meteora/clmm-routes/openPosition.ts Outdated
Comment thread docs/cross-venue-pool-normalisation.md Outdated
Comment thread docs/cross-venue-pool-normalisation.md Outdated
@fengtality
fengtality force-pushed the fix/solana-fork-fixes branch from 1b216c7 to 5e122db Compare October 20, 2026 02:40
Comment thread src/chains/solana/solana-activity.ts
Comment thread src/chains/solana/solana.ts
@fengtality
fengtality force-pushed the fix/solana-fork-fixes branch from 5e122db to 3e4e74d Compare October 20, 2026 02:50
Comment thread src/chains/solana/solana.ts
…ts symbol

getBalances reported a listed token under its symbol when asked for by
symbol, but under its mint when asked for by mint, so the caller could not
name or price it. Both now report the symbol. A wallet with no token
accounts goes through the same steps instead of its own shortcut, so the
keys no longer depend on whether the wallet holds the token.
…s mints

The DLMM add-liquidity route asked getBalances for the tokens' symbols. A
token outside the token list has no symbol getBalances knows (getToken names
it DUMMY_..., or the route falls back to UNKNOWN), so its balance read as 0
and a funded wallet was refused with 'Insufficient balance'. Ask by mint,
read each side under the key getBalances reports it by, and treat the native
side by its mint for the transaction buffer.
… fails

A wide DLMM open creates the position in one transaction and funds it over
several. When the first deposit chunk failed, the route left an empty
position behind, still holding its rent. It now reads the position and, if
it holds nothing, closes it with closePositionIfEmpty and says so in the
error. A send can fail after its chunk was broadcast and still land, so a
position that holds tokens is left open and the error says what it holds.
If the read or the close fails, the error names the position.
… activity

Each of a wallet's recent transactions is named as what it did: swap,
open_position, add_liquidity, remove_liquidity, collect_fees,
close_position, deposit or withdraw. It covers the Solana venues Gateway
trades on: Meteora DLMM and DAMM v2, Raydium CLMM, CPMM and AMM v4, Orca
Whirlpools, PancakeSwap's Solana CLMM, and Jupiter.

Two readings are joined per transaction. The venue instruction is matched by
program id and discriminator on the outer instructions and the inner CPIs,
with Anchor discriminators derived from instruction names
(sha256("global:<name>")) and pinned in a test against published bytes.
What moved is read from the wallet's own pre and post balances, SOL included
and the fee kept out. With no venue, the tokens that moved decide deposit or
withdraw (the SOL a new token account costs is not a withdrawal), and a wrap
or unwrap is neither.

Transactions are read five at a time rather than in one JSON-RPC batch,
which not every RPC honours. The venue tables hold mainnet program ids, so
another cluster answers 400. A malformed address answers 400, and Ethereum
answers 501. The spec test for the signing gate lists GETs that return
signatures separately and exactly, since a GET signs nothing.
Base and quote resolve in the venue's own order, and the venues disagree:
Meteora usually puts SOL on the quote side, Orca and Raydium on the base
side. Requiring one orientation empties most of Orca's and Raydium's SOL
pools, and a caller that always puts its SOL budget in one amount field
sends it to the wrong token on half of them.

Orca returns tradeEnableTimestamp as the unix epoch on every pool, so every
Orca pool looks decades old and pool-age filters silently pass or drop the
whole venue.

Also notes that the yield figures Gateway passes through are each venue's
own quantity, not normalised.
fetchTokenAccounts dropped every token account whose mint is not in the
token list, so getBalances reported 0 for an unlisted mint the wallet holds,
and Meteora add-liquidity still refused a funded wallet for one. Keep those
accounts; the balance uses the decimals read from the mint through the token
program that owns the account. A mint that cannot be read is now an error
instead of a balance computed with a guessed 9 decimals after a 1 s timeout.
The default list-only view is unchanged.

The Meteora add-liquidity test now runs the real fetchTokenAccounts over
encoded token accounts instead of a stub.
@fengtality

Copy link
Copy Markdown
Contributor Author

Bundled into #735 with the other fix PRs.

@fengtality fengtality closed this Oct 20, 2026
@fengtality
fengtality deleted the fix/solana-fork-fixes branch October 20, 2026 19:20
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