Debugging and Troubleshooting
Debugging OXN transactions is mostly like debugging Ethereum — same gas concepts, same reverts, same event logs — with one big twist: revert reasons are encrypted. If a transaction fails, you cannot read the error message from the outside. This page covers the practical debugging playbook.
Reading revert reasons
On Ethereum: if a call reverts with require(false, "not authorized"), block explorers show "not authorized" in the transaction details.
On OXN: the same call reverts with an encrypted revert reason. Explorers see ciphertext. Only the caller who submitted the transaction can decrypt the error.
How to see the actual reason
Use a wrapped provider that has the session key:
try {
await token.transfer(recipient, amount, { gasLimit: 500_000n });
} catch (err) {
console.log(err.reason); // "not authorized" — decrypted by the wrapped provider
console.log(err.data); // full error object
}
If the caller is your own client (which is usually the case), you have the key. If the caller is a user and you're debugging their failure from your side, you don't have the key — you have to ask them for the error, or reproduce the failure yourself.
Debugging user-reported failures
When a user reports "my transaction failed", your options are:
- Ask the user to check their wallet. MetaMask / OXN Wallet show the decrypted revert reason if they were the caller.
- Reproduce with the same inputs. Sign a call from your own address using the same arguments and see what reverts.
- Add better on-chain assertions. Custom errors (
error NotAuthorized(address caller);) with structured fields are easier to identify than string reasons.
Common reverts and their causes
invalid code — Compiled with evmVersion newer than paris. Contains PUSH0 (or another unsupported opcode) that the runtime cannot execute. See EVM Compatibility. Recompile with evmVersion: "paris".
out of gas — Set gasLimit too low. Bump it. Common defaults are documented in Gas and Fees.
insufficient balance to pay fees — Account balance too low. Faucet more or use gasPrice: 0n on testnet.
"Invalid address" in the wallet UI — Might actually be insufficient balance — the OXN Web Wallet mislabels this error in some versions. Check the underlying transaction status via a script or the explorer before concluding the address is bad.
Call returns default value (0, empty string, zero address) unexpectedly — You sent a plain eth_call instead of a signed query. Confirm the contract was constructed with a signer, not a raw provider. See Signed Queries.
Deployment succeeds but getCode returns 0x — Gas ran out during deployment. Bump gasLimit to 5_000_000n or more and redeploy.
Tracing transactions
For deeper debugging, use debug_traceTransaction:
const trace = await rawProvider.send("debug_traceTransaction", [
txHash,
{ tracer: "callTracer" }
]);
console.log(JSON.stringify(trace, null, 2));
This shows the call stack and gas usage per frame. On OXN, encrypted calldata appears as opaque hex bytes in the trace unless you're running it from the caller's context.
Reading events from a failed transaction
Failed transactions do not emit events (Solidity emits are rolled back on revert). What you see in getTransactionReceipt(txHash).logs is empty when a transaction failed. The revert reason is in the receipt's status:
const receipt = await provider.getTransactionReceipt(txHash);
if (receipt.status === 0) {
console.log("Transaction failed");
}
For decrypted reason, catch the error from the original send, as shown above.
Console.log for Solidity
Hardhat provides console.log for Solidity that works in tests:
import "hardhat/console.sol";
contract MyContract {
function doThing(uint256 x) external {
console.log("Received:", x);
console.log("Sender :", msg.sender);
}
}
These logs appear in your Hardhat test output. They are stripped during compilation for deployment, so no gas cost on real networks. However, console.log output on real OXN is not available — it works only in local Hardhat network.
Debugging with Foundry traces
Foundry's forge test -vvvv prints a full execution trace of each test:
forge test -vvvv --match-test testMyFailure
Shows every call, revert, log, and gas per frame. Locally.
Explorer trace tools
The block explorer at explorer.bout.network shows transaction traces (call flow, gas per frame) for public data. Encrypted calldata and events show as ciphertext in the explorer UI. Use it for the "did the transaction execute at all, and what path did it take" question; use your local debugger for "why did it fail".
Common integration debugging patterns
const raw = new ethers.JsonRpcProvider("https://rpc.bout.network");
const tx = await raw.getTransaction(txHash);
const firstByte = parseInt(tx.data.slice(2, 4), 16);
console.log("Encrypted:", (firstByte & 0xe0) === 0xa0);
const asOwner = await wrapped.getTransactionReceipt(txHash); // logs decrypted
const asOutsider = await raw.getTransactionReceipt(txHash); // logs ciphertext
console.log("Decrypted event count:", asOwner.logs.length);
console.log("Raw event topic 0: ", asOutsider.logs[0].topics[0]);
try {
const result = await contract.myFunction.staticCall(args);
console.log("Would return:", result);
} catch (err) {
console.log("Would revert with:", err.reason);
}