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)