11import "server-only" ;
22import type { Hex } from "viem" ;
3- import { createPublicClient , encodeFunctionData , http } from "viem" ;
3+ import { BaseError , createPublicClient , encodeFunctionData , http } from "viem" ;
44import {
55 checkGasCredits ,
66 getGasTokenPriceUsd ,
@@ -12,6 +12,7 @@ import { MetricNames } from "@/lib/metrics/types";
1212import { isTestnetChain } from "@/lib/web3/chainlink-feeds" ;
1313import { createSponsoredClient } from "@/lib/web3/sponsored-client" ;
1414import { isGasSponsorshipEnabled } from "@/lib/web3/sponsorship-feature-flag" ;
15+ import { SponsoredTxRevertError } from "@/lib/web3/turnkey-revert" ;
1516import { submitTurnkeySponsoredTransaction } from "@/lib/web3/turnkey-sponsored-tx" ;
1617import { isSponsorshipSupported } from "@/lib/web3/turnkey-sponsorship-config" ;
1718
@@ -97,6 +98,7 @@ export async function executeSponsoredTransaction(
9798
9899 return await finalizeSponsoredTx (
99100 submitResult . txHash ,
101+ submitResult . sendTransactionStatusId ,
100102 params . rpcUrl ,
101103 params . organizationId ,
102104 params . chainId ,
@@ -156,13 +158,58 @@ export async function executeSponsoredContractTransaction(
156158
157159 return await finalizeSponsoredTx (
158160 submitResult . txHash ,
161+ submitResult . sendTransactionStatusId ,
159162 params . rpcUrl ,
160163 params . organizationId ,
161164 params . chainId ,
162165 params . executionId
163166 ) ;
164167}
165168
169+ // viem renders an Error(string) revert as
170+ // "Execution reverted with reason: <reason>." -- pull <reason> back out.
171+ const VIEM_REVERT_REASON_RE = / r e v e r t e d w i t h r e a s o n : \s * ( .+ ?) \. ? \s * $ / i;
172+
173+ /**
174+ * Recover the revert reason of an already-mined, reverted sponsored tx.
175+ *
176+ * This is a read-only `eth_call` replay against the block the tx landed in --
177+ * it never broadcasts a second transaction, so it does NOT double-send. Returns
178+ * undefined when the reason cannot be decoded (custom error without an ABI, or
179+ * state drift so the replay no longer reverts); callers fall back to a generic
180+ * message.
181+ */
182+ async function decodeSponsoredRevertReason (
183+ publicClient : ReturnType < typeof createPublicClient > ,
184+ txHash : Hex ,
185+ blockNumber : bigint
186+ ) : Promise < string | undefined > {
187+ let tx : Awaited < ReturnType < typeof publicClient . getTransaction > > ;
188+ try {
189+ tx = await publicClient . getTransaction ( { hash : txHash } ) ;
190+ } catch {
191+ return ;
192+ }
193+ if ( ! tx . to ) {
194+ return ;
195+ }
196+ try {
197+ await publicClient . call ( {
198+ account : tx . from ,
199+ to : tx . to ,
200+ data : tx . input ,
201+ value : tx . value ,
202+ blockNumber,
203+ } ) ;
204+ return ;
205+ } catch ( replayError ) {
206+ if ( replayError instanceof BaseError ) {
207+ return VIEM_REVERT_REASON_RE . exec ( replayError . shortMessage ) ?. [ 1 ] ?. trim ( ) ;
208+ }
209+ return ;
210+ }
211+ }
212+
166213/**
167214 * Wait for receipt, record gas usage, and build the result.
168215 *
@@ -173,6 +220,7 @@ export async function executeSponsoredContractTransaction(
173220 */
174221async function finalizeSponsoredTx (
175222 txHash : Hex ,
223+ sendTransactionStatusId : string ,
176224 rpcUrl : string ,
177225 organizationId : string ,
178226 chainId : number ,
@@ -187,7 +235,21 @@ async function finalizeSponsoredTx(
187235 } ) ;
188236
189237 if ( receipt . status !== "success" ) {
190- throw new Error ( `Sponsored transaction reverted: ${ txHash } ` ) ;
238+ // The sponsored tx is on-chain and reverted. Read-only replay to recover
239+ // the reason (Turnkey's early hash meant we never saw its FAILED status),
240+ // then raise the typed revert error -- not a generic Error -- so the caller
241+ // reports it as the node result instead of falling back and duplicating.
242+ const reason = await decodeSponsoredRevertReason (
243+ publicClient ,
244+ txHash ,
245+ receipt . blockNumber
246+ ) ;
247+ throw new SponsoredTxRevertError ( {
248+ message : reason ?? "reason unavailable" ,
249+ txHash,
250+ sendTransactionStatusId,
251+ revertChain : [ ] ,
252+ } ) ;
191253 }
192254
193255 const gasUsed = receipt . gasUsed ;
0 commit comments