Summary
@circle-fin/x402-batching 3.2.0 wraps every waitForTransactionReceipt exception in GatewayClient.withdraw() as Mint transaction failed: <hash>, even when the underlying transaction succeeded onchain. At the same time, it never inspects receipt.status, so a transaction that actually reverted is reported as a successful withdrawal. The false-failure case is intermittent in the wild (it depends on RPC receipt propagation), but I include a deterministic repro below, and the unchecked-revert case is visible by code inspection.
I first hit this in production on Arc Testnet while building an x402 marketplace: the SDK threw Mint transaction failed: 0x4dce8d4f037369aad86f2711023605c15275d0e6a73d460771f9733475b30759, but that tx was confirmed onchain with status Success, no revert, gas 141697/143989 (98.41%).
Onchain evidence: https://testnet.arcscan.app/tx/0x4dce8d4f037369aad86f2711023605c15275d0e6a73d460771f9733475b30759
Affected code
src/client/GatewayClient.ts, inside withdraw() (also reached via the deprecated transfer() alias):
try {
await destPublicClient.waitForTransactionReceipt({ hash: mintTxHash });
} catch (err) {
throw new Error(`Mint transaction failed: ${mintTxHash}`, { cause: err });
}
Two problems with this pattern:
-
False failure on transient errors. viem's waitForTransactionReceipt throws on timeouts and RPC errors (for example a transient 5xx or receipt propagation lag exhausting the default retry budget). None of these mean the mint failed. The tx was already broadcast and, in both cases below, already confirmed. Callers who trust this error and trigger refund or retry logic risk double payouts.
-
Real reverts pass silently. viem does not throw when a transaction reverts. It resolves with receipt.status === 'reverted'. Since the code only catches exceptions and never checks the status field, an actually reverted mint returns a successful WithdrawResult with a mintTxHash pointing at a failed tx.
The same catch-without-status-check pattern repeats in deposit(), depositFor(), initiateTrustlessWithdrawal() and completeTrustlessWithdrawal().
Deterministic repro
Observed naturally in production on July 15 (evidence above). To isolate the SDK's error handling, I also built a fault-injection repro: a local JSON-RPC proxy that forwards every request to https://rpc.testnet.arc.network unchanged, except eth_getTransactionReceipt, which gets an injected HTTP 500 — simulating the transient receipt-lookup failures public RPCs produce under load.
Running GatewayClient.transfer('0.0009', 'arcTestnet', <own address>) through that proxy on 3.2.0:
[repro] SDK threw as expected:
Mint transaction failed: 0xab19fc6283d7da40803d3d917fbe6635e90b5ded3215dc15c157a37ddcaafd69
[repro] verifying on-chain via direct RPC (bypassing proxy)
[repro] on-chain receipt: status=success gasUsed=135209
[repro] CONFIRMED: SDK reported failure but tx succeeded on-chain.
Repro tx: https://testnet.arcscan.app/tx/0xab19fc6283d7da40803d3d917fbe6635e90b5ded3215dc15c157a37ddcaafd69
Happy to share the proxy + repro scripts if useful.
Expected behavior
- A receipt-wait exception should surface as an indeterminate/receipt-timeout error (ideally with the tx hash in a structured field), not as a definitive mint failure.
receipt.status should be checked after a successful wait, and 'reverted' should be the case that produces a failure.
Suggested fix
let receipt;
try {
receipt = await destPublicClient.waitForTransactionReceipt({ hash: mintTxHash });
} catch (err) {
throw new Error(
`Mint receipt not confirmed within timeout (tx may still succeed): ${mintTxHash}`,
{ cause: err },
);
}
if (receipt.status === 'reverted') {
throw new Error(`Mint transaction reverted: ${mintTxHash}`);
}
Exposing receipt timeout and confirmation count via GatewayClientConfig would also help consumers on chains with slower or flakier public RPCs.
Workaround I am using
In my payment path I catch the error, extract the hash with /Mint transaction failed: (0x[a-fA-F0-9]{64})/, and poll the receipt myself with a 60s budget. status === 'success' recovers the payment, status === 'reverted' stays a failure. The workaround commit (July 15) documents the error as first encountered: sharken3948/Mahshar@2815dc0
Happy to test a fix.
Environment
Summary
@circle-fin/x402-batching3.2.0 wraps everywaitForTransactionReceiptexception inGatewayClient.withdraw()asMint transaction failed: <hash>, even when the underlying transaction succeeded onchain. At the same time, it never inspectsreceipt.status, so a transaction that actually reverted is reported as a successful withdrawal. The false-failure case is intermittent in the wild (it depends on RPC receipt propagation), but I include a deterministic repro below, and the unchecked-revert case is visible by code inspection.I first hit this in production on Arc Testnet while building an x402 marketplace: the SDK threw
Mint transaction failed: 0x4dce8d4f037369aad86f2711023605c15275d0e6a73d460771f9733475b30759, but that tx was confirmed onchain with status Success, no revert, gas 141697/143989 (98.41%).Onchain evidence: https://testnet.arcscan.app/tx/0x4dce8d4f037369aad86f2711023605c15275d0e6a73d460771f9733475b30759
Affected code
src/client/GatewayClient.ts, insidewithdraw()(also reached via the deprecatedtransfer()alias):Two problems with this pattern:
False failure on transient errors. viem's
waitForTransactionReceiptthrows on timeouts and RPC errors (for example a transient 5xx or receipt propagation lag exhausting the default retry budget). None of these mean the mint failed. The tx was already broadcast and, in both cases below, already confirmed. Callers who trust this error and trigger refund or retry logic risk double payouts.Real reverts pass silently. viem does not throw when a transaction reverts. It resolves with
receipt.status === 'reverted'. Since the code only catches exceptions and never checks the status field, an actually reverted mint returns a successfulWithdrawResultwith amintTxHashpointing at a failed tx.The same catch-without-status-check pattern repeats in
deposit(),depositFor(),initiateTrustlessWithdrawal()andcompleteTrustlessWithdrawal().Deterministic repro
Observed naturally in production on July 15 (evidence above). To isolate the SDK's error handling, I also built a fault-injection repro: a local JSON-RPC proxy that forwards every request to
https://rpc.testnet.arc.networkunchanged, excepteth_getTransactionReceipt, which gets an injected HTTP 500 — simulating the transient receipt-lookup failures public RPCs produce under load.Running
GatewayClient.transfer('0.0009', 'arcTestnet', <own address>)through that proxy on 3.2.0:Repro tx: https://testnet.arcscan.app/tx/0xab19fc6283d7da40803d3d917fbe6635e90b5ded3215dc15c157a37ddcaafd69
Happy to share the proxy + repro scripts if useful.
Expected behavior
receipt.statusshould be checked after a successful wait, and'reverted'should be the case that produces a failure.Suggested fix
Exposing receipt timeout and confirmation count via
GatewayClientConfigwould also help consumers on chains with slower or flakier public RPCs.Workaround I am using
In my payment path I catch the error, extract the hash with
/Mint transaction failed: (0x[a-fA-F0-9]{64})/, and poll the receipt myself with a 60s budget.status === 'success'recovers the payment,status === 'reverted'stays a failure. The workaround commit (July 15) documents the error as first encountered: sharken3948/Mahshar@2815dc0Happy to test a fix.
Environment