DEV Community

Cover image for Testing for Failed Transactions: A Builder's Checklist for STON.fi Integrations
Web3KD
Web3KD

Posted on

Testing for Failed Transactions: A Builder's Checklist for STON.fi Integrations

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”β–”

πŸ”Ž 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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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

  1. Every documented exit code your integration is likely to hit, logged with enough context to debug without re-running the transaction.
  2. Both refund paths β€” default sender refund and an explicitly specified refundAddress β€” verified independently.
  3. 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

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)