Most swap tutorials end at the happy path — quote comes back, user confirms, transaction lands, balance updates. Ship that alone and you've tested maybe 60% of what your integration will actually encounter in production. TON's transaction model is asynchronous and message-based in a way Ethereum developers in particular tend to underestimate: a "failed" swap on TON isn't a single clean revert, it's a cascade of messages that can partially succeed, bounce, get refunded as the wrong token, or silently eat gas without telling anyone why. This checklist is built around the specific ways STON.fi and Omniston integrations actually fail — not hypothetical edge cases, but the documented, reproducible ones — so you can write tests against them before your users find them for you.

"The failure you didn't write a test for isn't rare — it's just the one you haven't hit in staging yet." — a note from the trenches


Why TON Failures Don't Look Like EVM Failures

If your testing instincts come from Solidity, recalibrate them here. On Ethereum, a reverted transaction rolls back cleanly — state changes, gas spent, done. On TON, a "transaction" is really a chain of asynchronous messages between contracts, and each one goes through its own sequence of phases: storage (fees for the space a contract occupies), credit (crediting the incoming message value), compute (running the actual contract logic), action (executing whatever the compute phase decided to do — sending messages, updating state), and bounce (firing only if compute failed and the inbound message had the bounce flag set).

That last phase is the one that trips people up. A bounce isn't a revert — it's a new message sent back to the original sender, and it only happens automatically if the message was sent in bounceable mode with enough remaining balance to cover the bounce itself. If your integration's logic assumes "the transaction either fully succeeds or the user gets their money back exactly as before," you'll eventually hit a case where that's false — because a bounced message can't itself be bounced again, and a bounce triggered by a genuine out-of-gas condition may never get delivered at all.

There's a second wrinkle specific to jetton transfers: if your contract receives a TransferNotification for incoming jettons and that handler throws, bouncing the message is often the worst outcome — the standard jetton wallet contract has no defined behavior for un-bouncing tokens that already moved, meaning a naive bounce-on-error pattern can leave jettons stuck permanently. The fix is a try/catch around notification handling rather than letting an exception propagate into an automatic bounce — a detail that's easy to miss if you copy a generic "throw on invalid input" pattern from EVM-style contract code.


The Failure Taxonomy: What Actually Breaks

1. Slippage-Triggered Refunds

This is the most common "failure" your users will hit, and it's not really a bug — it's the protection working as intended. If the pool's price moves past the max_price_slippage_bps you set before the swap settles, the router refunds the input rather than executing at a worse price than agreed. STON.fi's own Omniston API defaults slippage to a value expressed in basis points — 100 means 1% — and it's worth testing both ends of this deliberately: too tight, and normal volatility triggers refunds constantly; too loose, and you've quietly removed the protection slippage exists for in the first place.

// Test both extremes explicitly, don't just pick one "reasonable" value const tightSlippage = 10; // 0.1% — expect frequent refunds on volatile pairs const looseSlippage = 500; // 5% — expect rare refunds, worse realized prices

2. Multi-Hop and Cross-Router Swaps Don't Refund Cleanly

If there's no direct pool for your pair, STON.fi routes through an intermediary token — sometimes across multiple routers. Here's the detail worth testing specifically: because multi-contract transactions on TON aren't atomic, a cross-swap that fails partway through can't be fully refunded in the original input token. Per STON.fi's own documentation, if the trade fails after the first hop has already executed, the user receives the intermediate token from that hop, not their original asset back. A test suite that only checks "did the user get token B or token A back" will miss this — you need a third assertion for "did the user get stuck holding the intermediate token instead."

3. Gas Miscalculation and Exit Code 11

Real-world example, pulled from a documented STON.fi SDK issue: a developer's swap transaction was accepted and broadcast successfully, but failed on-chain with exit_code: 11 — a generic compute-phase failure — traced back to a gas/parameter mismatch in how the swap transaction was constructed for a jetton-to-jetton pair specifically (as opposed to the TON-to-jetton path, which worked fine in the same codebase). The lesson generalizes: "the SDK call didn't throw" is not the same as "the swap will succeed." Any integration test suite needs a step that checks the actual on-chain exit code after submission, not just that sendTransaction() resolved without an error.

"I've lost more debugging hours to a transaction that broadcasts cleanly and fails silently on-chain than to any error the SDK actually threw at me." — a note from the trenches

4. TonConnect-Level Rejections (Before the Chain Ever Sees It)

A meaningful share of "failed transactions" never reach a block at all — they fail at the wallet layer. TonConnect's documented failure surface includes a specific, testable set of cases:

  • An address sent in raw format (0:abc…) where the wallet expected the friendly, bounceable-flag-encoded format

  • A payload that includes both messages and items in the same request — the spec requires exactly one, never both

  • valid_until already in the past by the time the wallet receives the request

  • A network mismatch between what the dApp requested and what the wallet has selected

  • The user simply rejecting the transaction — which TonConnect's own guidance says should be treated as a "changed their mind" path, shown as a soft message, and explicitly not logged as a system error

That last point matters for your monitoring, not just your tests: if user rejections and genuine on-chain failures land in the same error bucket in your dashboards, you'll chase phantom bugs that are really just people closing the confirmation dialog.

async function handleSwap() { try { await tonConnectUI.sendTransaction(txRequest); } catch (err) { if (isUserRejection(err)) { setStatus("Transaction cancelled"); // not an error — don't alert on this return; } reportFailure(err); // genuine failure — this one goes to monitoring } }

5. Stale Quotes on the Omniston Side

Because useRfq() is a live subscription, a quote your UI displayed ten seconds ago may no longer be the one Omniston actually settles against if you don't rebuild the transaction from the latest event. Test this by deliberately introducing latency between "user sees a quote" and "user confirms" in your test harness — a few seconds is enough on a volatile pair to demonstrate the gap between a stale UI value and what actually executes.


A Reference Table Worth Keeping Nearby

TON reserves exit codes 0–127 for protocol-level meanings; codes 256–65535 are open for contract-specific errors, which means the same exit code can mean something different depending on which contract threw it — always confirm the meaning against the specific contract's own documentation rather than assuming a code means the same thing everywhere.


Building an Actual Test Matrix

Rather than testing "does a swap work," structure your suite around the failure taxonomy above. A reasonable matrix for a swap feature looks like this:

  1. Happy path — quote, build, sign, settle, balance updates correctly

  2. Slippage refund (tight) — deliberately set slippage low enough on a live-ish sandbox environment to force a refund, confirm the user gets their original input back

  3. Slippage refund (edge) — set slippage to the exact boundary and confirm consistent behavior, not flakiness

  4. Multi-hop partial-fill scenario — simulate a cross-swap failing after the first hop, confirm your UI correctly represents "you received the intermediate token" rather than showing a generic failure

  5. Wallet rejection — simulate the user declining in the wallet UI, confirm it's tracked as a cancellation, not an error

  6. Expired valid_until — construct a transaction with a validity window already in the past, confirm your UI handles the resulting wallet-level rejection gracefully

  7. Stale quote — inject artificial delay between quote display and confirm, verify you're building against the latest quote event, not a cached one

  8. Malformed address format — feed a raw-format address into a sendTransaction call intentionally, confirm your address-formatting layer catches it before it reaches the wallet

// Sandbox-based unit test skeleton for the slippage-refund case import { Blockchain } from "@ton/sandbox"; describe("swap slippage protection", () => { it("refunds input when price moves past max_price_slippage_bps", async () => { const blockchain = await Blockchain.create(); const trader = await blockchain.treasury("trader"); // Simulate a pool state that has moved since quote issuance // ...set up router/pool contracts with adjusted reserves... const result = await sendSwap(blockchain, trader, { maxPriceSlippageBps: 10, // intentionally tight }); expect(result.refunded).toBe(true); expect(result.refundedAsset).toEqual(inputAsset); // not the intermediate token }); });


Monitoring After You Ship

Tests catch what you thought to test for. Production catches everything else. A few numbers worth tracking on an ongoing basis once real traffic hits your integration:

  • Refund rate as a percentage of attempted swaps, segmented by pair — a sudden spike on one specific pair usually means either a liquidity change or your default slippage setting is now too tight for that pair's current volatility

  • Wallet-rejection rate vs. genuine on-chain failure rate, tracked as separate metrics — conflating them, as noted above, wastes debugging time on a "problem" that's actually users changing their minds

  • Exit code distribution for any failures that do reach the chain — a new exit code appearing that wasn't in your test matrix is a signal to go add it, not just a data point to log and forget

"A test suite that never grows after launch is testing yesterday's failure modes, not today's." — a note from the trenches


The Checklist, Compressed

  • [ ] Test slippage refunds at tight, loose, and boundary values — not just one "reasonable" default

  • [ ] Explicitly test the multi-hop partial-refund case — assert on the intermediate-token outcome, not just success/failure

  • [ ] Verify on-chain exit codes post-submission, not just that the SDK call resolved

  • [ ] Handle jetton TransferNotification failures with try/catch, not bounce-on-error

  • [ ] Separate wallet-rejection tracking from genuine on-chain failure tracking, in both tests and production monitoring

  • [ ] Test with an intentionally expired valid_until to confirm graceful handling

  • [ ] Test address-format validation before a request ever reaches TonConnect

  • [ ] Rebuild transactions from the latest quote event, and test for the stale-quote case directly

  • [ ] Track refund rate, rejection rate, and exit-code distribution after launch — not just uptime


Wrapping Up

None of the failure modes above are exotic — they're documented, reproducible, and, in at least one case, sitting in a public GitHub issue with the exact exit code attached. What makes them worth a dedicated checklist is that TON's asynchronous, message-based execution model produces failure shapes that don't map cleanly onto EVM-trained intuition: partial fills that land you in the wrong token, bounces that can't be un-bounced, wallet-level rejections that never touch a block at all. Test for the specific way STON.fi and Omniston fail, not just the generic way swaps fail, and the gap between your staging environment and your first angry support ticket gets a lot smaller.


This checklist is based on TON and STON.fi/Omniston documentation, plus publicly documented integration issues, as of mid-2026. Exit code meanings can vary by contract, and API parameter names/formats evolve — verify specifics against current docs.ton.org and docs.ston.fi before relying on them in a production test suite.

$XRP

XRP
XRPUSDT
1.0001
+0.02%