Frontend
How to Handle a Failed STON.fi Swap in an App
Ivan “Crypto Vazima” Zimanov DEV Community
6 views
How to distinguish wallet rejection, submission failure, on-chain execution failure, and a STON.fi refund without showing users the wrong status.
A failed STON.fi swap should not be handled as one generic exception. In an app, the failure can happen while you simulate the trade, build the transaction, ask the wallet to sign, submit the message to TON, or execute the swap through STON.fi contracts. Those stages have different meanings and require different recovery actions.
The most important rule is simple: a resolved TonConnect sendTransaction() call does not prove that the entire swap succeeded. It gives your app the BoC of the broadcast external message. Your app still needs to determine what happened on-chain before presenting the swap as complete.
Start by separating the failure stages
A "Swap failed" toast hides too much information. Your application should first identify where execution stopped.
Stage
What happened
What your app should do
Simulation
No usable quote was produced
Keep funds untouched and request a fresh simulation
Transaction building
SDK or contract parameters could not be created
Log the technical error and do not open the wallet
Wallet approval
The request was rejected or could not be sent
Return to a retryable pre-submission state
Submitted
Wallet signed and broadcast the message
Do not call the swap successful yet
On-chain execution
TON or STON.fi processing failed
Inspect the transaction trace and possible refund
Swap completed
Requested output reached the intended receiver
Mark the operation successful
That distinction matters particularly on TON because contract interactions are asynchronous. A swap can involve several internal messages instead of one atomic execution step. TON documentation explicitly notes that different recipient contracts process messages independently, and failures can produce bounced messages under the appropriate conditions.
Your frontend therefore needs at least one state between wallet approved and swap succeeded.
Design the swap as a state machine
A small state machine is more reliable than a single isLoading flag.
type SwapStatus =
| "idle"
| "simulating"
| "ready"
| "awaiting_wallet"
| "submitted"
| "confirming"
| "success"
| "refunded"
| "failed";
type SwapFailureStage =
| "simulation"
| "build"
| "wallet"
| "submission"
| "execution";
The critical transition is:
ready
-> awaiting_wallet
-> submitted
-> confirming
-> success | refunded | failed
Do not replace it with:
ready
-> awaiting_wallet
-> success
TonConnect's sendTransaction() asks the connected wallet to sign and broadcast the transaction. Its response contains a base64 BoC of the broadcast external message. TON documentation recommends using that BoC to find and inspect what happened on-chain.
This lets your UI say something accurate such as "Transaction submitted" while confirmation is pending rather than prematurely displaying "Swap successful."
Prevent avoidable failures before the wallet opens
Good failure handling starts before try/catch.
STON.fi's current v2 integration guidance recommends an API-driven workflow: simulate the swap, use the router information returned by the API, construct the matching contracts through dexFactory(), then build the transaction parameters. Hardcoding a Router can make an integration incompatible with Router versions or types that the API actually selected.
Before opening TonConnect, verify:
the connected wallet is on the expected network
the offer amount is positive and converted using the token's actual decimals
the wallet has the offered asset
sufficient TON is available for execution costs when required
the quote is still current
minAskAmount comes from the simulation you intend to execute
the Router and pTON contracts correspond to the simulation result
the transaction has not already been submitted
STON.fi's React quickstart follows the same broad sequence: fetch assets, simulate the trade, derive contracts from Router information, build the swap parameters, and finally pass the resulting message to TonConnect.
For the TonConnect request itself, validUntil is a Unix timestamp in seconds. A typical request looks like this:
const response = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300,
network: "-239",
messages: [
{
address: swapParams.to.toString(),
amount: swapParams.value.toString(),
payload: swapParams.body?.toBoc().toString("base64"),
},
],
});
Keeping quote creation and transaction submission close together also reduces the chance that market conditions change enough to violate the swap's minimum output.
Handle wallet errors without lying to the user
A wallet rejection is not an on-chain swap failure. No STON.fi troubleshooting is required if the transaction never reached the blockchain.
Keep the wallet layer separate:
try {
setStatus("awaiting_wallet");
const result = await tonConnectUI.sendTransaction(tx);
setStatus("submitted");
await confirmSwapOnChain(result.boc);
} catch (error) {
setStatus("failed");
handleWalletOrSubmissionError(error);
}
TonConnect defines errors for cases such as a rejected request, bad request, unsupported method, wrong network, or disconnected wallet. A user declining a transaction should normally produce something like "Transaction cancelled" rather than "STON.fi swap failed."
Also avoid automatic blind retries when submission is ambiguous. Current TonConnect UI documentation specifically warns that some connect-and-send flows can leave the app without a response even though the request may already have reached the wallet. Retrying silently could submit the operation twice.
Give the user an explicit retry action after you have checked whether a matching transaction already exists.
Verify the outcome on-chain after broadcast
Once TonConnect returns the BoC, move the operation to submitted or confirming.
Store enough context to investigate it:
const pendingSwap = {
walletAddress,
offerAddress,
askAddress,
offerUnits,
minAskUnits,
routerAddress: simulationResult.router.address,
submittedAt: Date.now(),
boc: result.boc,
};
Then use your TON data provider or indexer to locate the external message and follow the resulting trace.
Do not stop at the wallet transaction alone. TON ordinary transactions contain separate compute, action, and possible bounce phases. A contract can execute its compute phase but still encounter a later action failure. The aborted, compute result, action result, and bounce data help distinguish those cases.
For a swap, the application-level question is ultimately:
Did the intended output arrive, or did the STON.fi execution take another path such as a refund?
That is stronger evidence than simply observing that the initiating wallet transaction exists.
Read STON.fi refund signals correctly
STON.fi DEX v2 has explicit failure and refund mechanics. The Router payload includes a refund_address, and the contract API defines several operation codes that explain why swap execution could not continue.
Examples include:
swap_refund_reserve_err: insufficient liquidity for the swap
swap_refund_0_out: calculated output is zero
swap_refund_slippage: output is below the required minimum
swap_pool_locked: the pool is locked
transfer_bounce_low_gas: insufficient gas for the operation
transfer_bounce_invalid_pool: invalid pool configuration
transfer_bounce_tx_expired: the transaction expired at the Router
Slippage failure is especially important for applications. STON.fi's Pool specification states that a swap fails when the amount the user would receive is below min_out. That protects the user from accepting execution worse than the limit encoded in the transaction.
The recovery action is usually not "retry immediately with a larger slippage tolerance." Instead, fetch a new simulation, show the new expected output, and let the user make another decision.
There is another TON-specific complication with routed swaps. STON.fi documents that multi-contract transactions on TON are not atomic. If a cross-swap fails after an intermediate step, a complete rollback to the original asset is not always possible. The user can receive an intermediate routing token instead.
Your balance reconciliation logic should therefore check actual resulting assets instead of assuming every failed swap returns exactly the original token.
Following one failed swap from start to finish
Imagine your app is swapping Jetton A for Jetton B.
The app simulates the trade and receives a route with an expected output and minAskUnits. It uses the Router metadata from that simulation with dexFactory(), builds the STON.fi message, and opens TonConnect.
The user approves it.
At this point, your UI should display:
Swap submitted
Waiting for on-chain execution...
not:
Swap successful
While TON processes the message chain, the pool price changes. The amount available from the swap falls below the encoded minimum.
STON.fi can reject that execution condition as a slippage failure rather than deliver an output below the user's limit. Your monitoring layer detects the resulting execution or refund path. The app changes the status to refunded, refreshes balances, and explains what happened:
Swap was not executed because the available output
fell below your minimum. Funds were refunded.
Get a new quote before trying again.
Compare that with a wallet rejection:
Transaction cancelled.
No swap was submitted.
And with an unresolved submitted transaction:
Transaction submitted.
Confirmation is still pending.
Do not submit the same swap again yet.
Those messages describe three genuinely different states and prevent users from taking the wrong recovery action.
Practical takeaway: treat the STON.fi transaction builder, TonConnect, TON execution, and the final swap outcome as separate checkpoints. Simulate immediately before submission, build from current Router metadata, store the returned BoC, verify the resulting trace, refresh balances, and only display success after the requested asset outcome has been confirmed.
Frequently Asked Questions
Does sendTransaction() succeeding mean my STON.fi swap succeeded?
No. It means the wallet successfully returned the BoC for the transaction it signed and broadcast. Contract execution still occurs on TON. Your app should move to a submitted or confirming state and inspect the resulting on-chain transaction flow before declaring the swap successful.
Should I automatically retry every failed swap?
No. First determine whether the original transaction was never submitted, definitively failed, was refunded, or has an unknown outcome. Automatic retries are risky when submission status is uncertain because the first transaction may still have reached the network. A user-triggered retry after reconciliation is safer.
Why can a swap fail after the wallet approved it?
Wallet approval only authorizes and broadcasts the initial message. TON contracts then process the operation through their execution phases and internal messages. STON.fi can encounter conditions such as insufficient output, expired execution, insufficient gas, a locked pool, or another contract-level failure after approval.
What should I do when a swap fails because of slippage?
Request a new simulation and present the updated quote. Do not silently increase the user's slippage tolerance. In STON.fi, the minimum output is an execution constraint, and the swap can be refunded when calculated output falls below that minimum. Changing the tolerance changes the trade the user is agreeing to.
Can a failed STON.fi swap return a different token?
It can in a routed cross-swap. STON.fi explains that TON multi-contract transactions are not fully atomic, so if a routed swap fails after an intermediate conversion, the user may receive an intermediate token rather than a complete rollback into the original asset. Your app should reconcile actual balances after failure.
Should I hardcode the STON.fi Router address in my app?
For current production integrations, STON.fi recommends an API-driven approach. The simulation provides Router information that can be passed into dexFactory() so the SDK selects the appropriate contracts. This also reduces problems caused by Router versions or types changing while your application continues using an old hardcoded configuration.
What is the safest flow for handling a failed STON.fi swap in an app?
Simulate first, build the transaction from current Router metadata, request the wallet signature, mark the operation as submitted when TonConnect returns, verify the on-chain result, detect success or refund, refresh balances, and only then show the final status. If the outcome is uncertain, prevent blind retries until your app has reconciled the transaction.
Sources and Further Reading
STON.fi Swap Guide (React) - official example covering simulation, dynamic Router selection, SDK transaction building, and TonConnect submission
STON.fi Swap v2 SDK documentation - current API-driven workflow for building production swaps from simulation and Router metadata
STON.fi DEX v2 smart contracts - overview of v2 swap mechanics, refunds, deadlines, and production SDK guidance
STON.fi Router v2 - Router swap payload fields including minimum output, refund address, receiver, and execution deadline
STON.fi DEX v2 Op Codes - reference for successful swaps, slippage refunds, liquidity errors, low gas, expired transactions, and other execution outcomes
STON.fi Swap Examples - official diagrams and explanations for simple swaps, routed swaps, and refund behavior
TON Connect Send Transaction - official specification of transaction submission, returned BoC, request expiration, network selection, and wallet errors
TON Ordinary Transactions and Execution Phases - official TON documentation explaining compute, action, bounce, aborted transactions, and asynchronous message execution
Read original: https://dev.to/ivan_cryptovazimazima/how-to-handle-a-failed-stonfi-swap-in-an-app-47oe
← Previous
Social Sign-In Recovery — JWT Caching with Live Session Introspection
Next →
The Endpoint Administrator’s Guide to Zero Trust with Microsoft Intune
Related
Zero-Budget Web Dev: Moving from Discord/Drive to Google Sites
Frontend
0
DEV Community
My adaptive memory stayed empty in production, and it wasn't a bug
Frontend
0
DEV Community
MV3 Chrome Extensions — Everything That Broke and How I Fixed It
Frontend
2
Dev.to (EN Zone)
Authentication APIs Explained: Template-Owned US/EU Login OTP with SMS and Email Fallback
Frontend
5
Dev.to (EN Zone)
Comments0
No comments yet — be the first