Testing for Failed Transactions: A Builder's Checklist for STON.fi Integrations
Most integration bugs don't show up in code review. They show up in production, as a transaction that "just failed," with an exit code the team has to look up and a user asking why their swap didn't go through. This checklist exists to catch those failures before they ship — with the actual exit codes, the actual sandbox environment, and real failure patterns pulled from genuine integration issues developers have hit building on STON.fi.
🗨️ "For most integrations, we recommend using our Node.js SDK or React SDK. The SDKs handle WebSocket connections, quote streaming, transaction building, and error handling automatically." — STON.fi, Omniston Swap Overview documentation
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
🧯 Start With the Environment You're Actually Testing Against
Before writing a single test case, confirm which endpoint your code is hitting. This sounds obvious and is still the source of a surprising number of "why did this work yesterday" bugs.
- 1️⃣ Production:
wss://omni-ws.ston.fi— real liquidity, real funds, real consequences for a bad transaction - 2️⃣ Sandbox:
wss://omni-ws-sandbox.ston.fi— explicitly documented as for development and testing only
Running integration tests against production because the sandbox URL wasn't configured is a completely avoidable failure mode, and it's worth a dedicated environment-variable check in CI specifically to catch it before any test suite runs.
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
The Exit Codes Worth Knowing Before You Debug Blind
When a transaction fails on TON, it returns a numeric exit code — and knowing what a handful of the common ones actually mean turns a mystery failure into a five-minute fix.
🗨️ "If all funds of the inbound message have already been consumed and there are not enough funds to pay for the failed action... an error with exit code 37 is thrown: Not enough GRAMs." — TON Docs, TVM Exit Codes reference
| Exit Code | Meaning | Typical Cause |
|---|---|---|
| 32 | Invalid action list | A malformed or exotic cell in the action list |
| 37 | Not enough GRAMs | Insufficient TON to cover the action's cost |
| 38 | Not enough extra currencies | Insufficient balance of a non-TON currency the action needs |
| 39 | Outbound message doesn't fit into a cell | The message payload exceeds TON's cell size limits |
These aren't hypothetical — real developers integrating STON.fi's SDK have hit specific exit codes like 11 and 709 in production issues, tracing back to jetton wallet address resolution and reverse-swap parameter mistakes respectively. Logging the raw exit code, not just "transaction failed," is the single highest-leverage change most integrations can make to their error handling.
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
🩹 The Checklist: Failure Modes to Actually Test For
3️⃣ Test with minAskAmount set to zero, deliberately, once. This is a real mistake pulled directly from a public integration issue — a developer building a swap left minAskAmount: 0, which means no slippage protection exists at all. Test this scenario specifically to confirm your own code never ships with this default, since a zero minimum will "succeed" a transaction that delivers almost nothing back.
// ❌ Never ship this — no slippage protection at all
const txParams = await router.buildSwapJettonTxParams({
minAskAmount: '0', // any price gets accepted
// ...
});
// ✅ Always derive a real minimum from your slippage tolerance
const minAskAmount = calculateMinAskAmount(quote.askAmount, slippageBps);
4️⃣ Test insufficient gas explicitly. A swap transaction needs enough TON reserved to cover network fees on top of the trade itself. Write a test that intentionally underfunds the gas amount and confirms your app surfaces exit code 37 clearly, rather than a generic failure message.
async function testInsufficientGas() {
const result = await sendSwap({ ...validParams, gasAmount: '10000' }); // too low
expect(result.exitCode).toBe(37);
expect(result.userMessage).toMatch(/insufficient.*gas/i);
}
5️⃣ Test reverse-swap parameter direction. A real, documented issue came from treating a reverse swap (jetton-to-TON) as identical to a regular swap with askJettonAddress simply swapped to a proxy TON address — this produced exit code 709 for at least one developer. If your integration supports both directions, test each direction as a genuinely separate code path, not a parameter flip on the same function.
6️⃣ Test a stale or expired quote. Simulate a delay between requesting a quote and submitting the transaction, long enough that the quote should reasonably be considered outdated. Confirm your integration re-requests rather than blindly submitting stale terms.
async function testStaleQuote() {
const { quote, rfqId } = await getQuote(amount);
await sleep(60_000); // simulate user hesitation
const result = await executeSwap(quote, rfqId);
// Expect either a fresh re-quote or a clean, explicit failure —
// never a silent execution against 60-second-old terms
expect(['refreshed', 'expired']).toContain(result.status);
}
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
🔎 Verifying the Failure Path, Not Just the Success Path
A transaction that fails cleanly and refunds correctly is a successful outcome from a testing standpoint — it's the failures that fail silently or incorrectly that actually cause damage.
🗨️ "(Optional) The address where funds should be returned if the transaction fails. If not specified, funds are returned to the sender's wallet." — STON.fi, Omniston Swap Overview documentation
Test both branches of this explicitly:
describe('refund handling', () => {
it('returns funds to sender when refundAddress is omitted', async () => {
const result = await simulateFailedSwap({ refundAddress: undefined });
expect(result.refundedTo).toBe(senderWallet.address);
});
it('returns funds to the specified refundAddress when set', async () => {
const result = await simulateFailedSwap({ refundAddress: customAddress });
expect(result.refundedTo).toBe(customAddress);
});
});
Skipping this test specifically is how teams discover, in production, that an edge case in their refund logic sent funds somewhere unexpected — exactly the kind of bug that's cheap to catch in sandbox and expensive to discover after a real user reports missing funds.
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
🛡️ Comparing on the Dimensions That Actually Matter
💧 SDK-handled errors vs. low-level protocol errors. STON.fi's own documentation is explicit that the Node.js and React SDKs handle error handling automatically for most integrations — testing at the low-level WebSocket protocol layer is only necessary if you've deliberately opted out of that abstraction.
🧭 Sandbox coverage vs. production monitoring. Sandbox testing catches logic errors before deployment; it doesn't replace production monitoring for exit codes appearing at real volume, since some failure conditions — like transient network congestion — are difficult to reproduce reliably in a test environment.
⏱️ Synchronous failures vs. timing-dependent failures. A malformed parameter fails immediately and predictably. A stale quote or a race between simulation and execution only fails under specific timing conditions, which is exactly why deliberate delay-based tests matter more than they might seem to.
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
✅ What a Genuinely Solid Test Suite Covers
- Every documented exit code your integration is likely to hit, logged with enough context to debug without re-running the transaction.
-
Both refund paths — default sender refund and an explicitly specified
refundAddress— verified independently. -
At least one deliberately broken test per parameter that guards user funds, especially
minAskAmount, since a silent zero-default is a real, documented failure mode.
⚠️ What's Worth Understanding Correctly
- A generic "transaction failed" message is a testing gap, not a UI limitation. The exit code is available; surfacing it (or a translation of it) is a choice, not a constraint.
- Sandbox and production are genuinely separate environments, not a toggle on the same endpoint. Confirm which one every test run is actually targeting.
- Real GitHub issues are a legitimate test-case source. Public integration problems other developers have hit are, functionally, a pre-written regression test suite for exactly the mistakes worth guarding against.
🏁 Bottom Line
Testing for failed transactions on STON.fi isn't about imagining hypothetical edge cases — it's about deliberately reproducing the specific, documented failure modes that real integrations have already hit: zero-slippage-protection defaults, insufficient gas, reverse-swap parameter mix-ups, stale quotes, and untested refund paths. A checklist built from real exit codes and real reported issues catches these before a user does, which is the entire point of testing a payment-adjacent integration in the first place.
🔗 Sources & Further Reading
- TON Docs — TVM Exit Codes Reference — https://docs.ton.org/v3/documentation/tvm/exit-codes
- STON.fi — Omniston Swap Overview (sandbox/production endpoints, refundAddress) — https://docs.ston.fi/developer-section/omniston/swap/overview
- STON.fi SDK — GitHub Issues (real reported integration failures) — https://github.com/ston-fi/sdk/issues
- STON.fi — React Swap Quickstart Guide — https://docs.ston.fi/developer-section/quickstart/swap
- STON.fi — SDK v2 Swap Documentation — https://docs.ston.fi/developer-section/dex/sdk/v2/swap
This article reflects independent research based on STON.fi's public developer documentation, TON's official exit code reference, and publicly reported integration issues as of mid-2026. Exit codes, SDK behavior, and endpoints evolve as the protocol ships updates — always verify current details directly on docs.ston.fi before shipping a production integration.


Top comments (0)