@@ -155,13 +155,14 @@ export async function getEvmTransactionInfoFromNetwork(
155155 } ,
156156 ) ;
157157 } catch ( e ) {
158- // If `getCode` fails, fall back to legacy behavior for safety
158+ // If `getCode` fails, we can't reliably determine if it's a contract.
159+ // For safety with AA wallets, skip nonce checks if we also have a high nonce or detected AA tx.
159160 logger . warn ( 'getTransactionInfoFromNetwork() getCode failed' , {
160161 error : e ?. message ,
161162 fromAddress : input . fromAddress ,
162163 networkId,
163164 } ) ;
164- shouldUseNonceChecks = typeof nonce === 'number' ;
165+ // Keep shouldUseNonceChecks as is, will be overridden by high nonce check if needed
165166 txTrace (
166167 input ,
167168 'getEvmTransactionInfoFromNetwork:nonceChecks:getCode_failed' ,
@@ -175,16 +176,18 @@ export async function getEvmTransactionInfoFromNetwork(
175176
176177 // If nonce is suspiciously high (> 100000), it's likely an AA wallet's internal nonce
177178 // rather than an EOA nonce. Skip the check to avoid false negatives.
179+ // This check MUST happen after getCode() attempt to override any previous setting.
178180 const isSuspiciouslyHighNonce = nonce && nonce > 100000 ;
179181
180182 if ( isSuspiciouslyHighNonce ) {
181183 logger . info (
182- 'Detected suspiciously high nonce (likely AA wallet), skipping nonce validation' ,
184+ 'Detected suspiciously high nonce (likely AA wallet), forcing nonce validation skip ' ,
183185 {
184186 nonce,
185187 txHash : input . txHash ,
186188 fromAddress : input . fromAddress ,
187189 networkId,
190+ previousShouldUseNonceChecks : shouldUseNonceChecks ,
188191 } ,
189192 ) ;
190193 txTrace ( input , 'getEvmTransactionInfoFromNetwork:high_nonce_skip' , {
@@ -491,30 +494,68 @@ function extractErc20TransferFromReceipt(params: {
491494 const expectedToLower = expectedTo . toLowerCase ( ) ;
492495 const expectedFromLower = expectedFrom ?. toLowerCase ( ) ;
493496
494- const candidates : Array < { from : string ; to : string ; amount : number } > = [ ] ;
497+ logger . debug ( 'extractErc20TransferFromReceipt: starting search' , {
498+ tokenAddress : tokenLower ,
499+ expectedTo : expectedToLower ,
500+ expectedFrom : expectedFromLower ,
501+ expectedAmount,
502+ totalLogsCount : logs . length ,
503+ } ) ;
504+
505+ // First pass: collect all Transfer events for this token to see what we have
506+ const allTokenTransfers : Array < { from : string ; to : string ; amount : number } > =
507+ [ ] ;
495508 for ( const log of logs ) {
496509 if ( ! log ?. topics || log . topics . length < 3 ) continue ;
497510 if ( ( log . address || '' ) . toLowerCase ( ) !== tokenLower ) continue ;
498511 if ( ( log . topics [ 0 ] || '' ) . toLowerCase ( ) !== ERC20_TRANSFER_TOPIC ) continue ;
499512
500513 const from = ( '0x' + log . topics [ 1 ] . slice ( 26 ) ) . toLowerCase ( ) ;
501514 const to = ( '0x' + log . topics [ 2 ] . slice ( 26 ) ) . toLowerCase ( ) ;
502- if ( to !== expectedToLower ) continue ;
503515
504516 let rawValue : string ;
517+ let amount : number ;
505518 try {
506- // log.data is 32-byte uint256
507519 rawValue = ethers . BigNumber . from ( log . data ) . toString ( ) ;
520+ amount = normalizeAmount ( rawValue , tokenDecimals ) ;
508521 } catch {
509522 continue ;
510523 }
511- const amount = normalizeAmount ( rawValue , tokenDecimals ) ;
512- candidates . push ( { from, to, amount } ) ;
524+ allTokenTransfers . push ( { from, to, amount } ) ;
513525 }
514526
515- if ( candidates . length === 0 ) return null ;
527+ logger . info ( 'extractErc20TransferFromReceipt: found token transfers' , {
528+ tokenAddress : tokenLower ,
529+ transfersCount : allTokenTransfers . length ,
530+ transfers : allTokenTransfers . slice ( 0 , 10 ) , // Log first 10 to avoid huge logs
531+ } ) ;
532+
533+ // Second pass: filter for expectedTo
534+ const candidates : Array < { from : string ; to : string ; amount : number } > =
535+ allTokenTransfers . filter ( t => t . to === expectedToLower ) ;
536+
537+ logger . info ( 'extractErc20TransferFromReceipt: filtered candidates' , {
538+ expectedTo : expectedToLower ,
539+ candidatesCount : candidates . length ,
540+ candidates,
541+ } ) ;
516542
517- // Prefer: (1) from matches expectedFrom (if provided) AND (2) amount close to expectedAmount (if provided)
543+ if ( candidates . length === 0 ) {
544+ logger . error (
545+ 'extractErc20TransferFromReceipt: no matching transfers found' ,
546+ {
547+ expectedTo : expectedToLower ,
548+ expectedAmount,
549+ allTransfersCount : allTokenTransfers . length ,
550+ allTransfers : allTokenTransfers ,
551+ } ,
552+ ) ;
553+ return null ;
554+ }
555+
556+ // Scoring: prioritize amount match (most important), then from match
557+ // For AA wallets, the 'from' in Transfer events is often the smart wallet, not the user EOA
558+ // So we weight amount match higher than from match
518559 const scored = candidates
519560 . map ( c => {
520561 const fromMatch = expectedFromLower
@@ -524,12 +565,29 @@ function extractErc20TransferFromReceipt(params: {
524565 typeof expectedAmount === 'number'
525566 ? closeTo ( c . amount , expectedAmount )
526567 : false ;
527- const score = ( fromMatch ? 2 : 0 ) + ( amountMatch ? 1 : 0 ) ;
568+ // Amount match = 3 points (most important), from match = 1 point
569+ const score = ( amountMatch ? 3 : 0 ) + ( fromMatch ? 1 : 0 ) ;
528570 return { c, score } ;
529571 } )
530572 . sort ( ( a , b ) => b . score - a . score ) ;
531573
532- return scored [ 0 ] . c ;
574+ const best = scored [ 0 ] ;
575+
576+ // If we have multiple candidates with same score, log a warning
577+ const topScore = best . score ;
578+ const tiedCandidates = scored . filter ( s => s . score === topScore ) ;
579+ if ( tiedCandidates . length > 1 ) {
580+ logger . warn ( 'Multiple ERC-20 transfers with same score found' , {
581+ tokenAddress,
582+ expectedTo : expectedToLower ,
583+ expectedFrom : expectedFromLower ,
584+ expectedAmount,
585+ candidatesCount : tiedCandidates . length ,
586+ topScore,
587+ } ) ;
588+ }
589+
590+ return best . c ;
533591}
534592
535593export async function findEvmTransactionByHash (
@@ -968,6 +1026,17 @@ async function getTransactionDetailForTokenTransfer(
9681026 // Account abstraction: the outer tx goes to the EntryPoint, but the actual ERC20 transfer happens
9691027 // inside the UserOperation. In that case we validate by inspecting ERC20 Transfer logs instead of tx.to.
9701028 if ( isEntryPointTx ) {
1029+ logger . info (
1030+ 'Detected AA EntryPoint transaction for ERC-20 token transfer' ,
1031+ {
1032+ txHash,
1033+ networkId,
1034+ entryPoint : transaction . to ,
1035+ token : token . symbol ,
1036+ tokenAddress : token . address ,
1037+ } ,
1038+ ) ;
1039+
9711040 const transfer = extractErc20TransferFromReceipt ( {
9721041 receipt,
9731042 tokenAddress : token . address ,
@@ -976,17 +1045,37 @@ async function getTransactionDetailForTokenTransfer(
9761045 expectedAmount : input . amount ,
9771046 tokenDecimals : token . decimals ,
9781047 } ) ;
1048+
9791049 if ( ! transfer ) {
1050+ logger . error (
1051+ 'No matching ERC-20 transfer found in AA transaction receipt' ,
1052+ {
1053+ txHash,
1054+ expectedTo : input . toAddress ,
1055+ expectedFrom : input . fromAddress ,
1056+ expectedAmount : input . amount ,
1057+ tokenAddress : token . address ,
1058+ logsCount : receipt . logs ?. length ,
1059+ } ,
1060+ ) ;
9801061 throw new Error (
9811062 i18n . __ (
9821063 translationErrorMessagesKeys . TRANSACTION_TO_ADDRESS_IS_DIFFERENT_FROM_SENT_TO_ADDRESS ,
9831064 ) ,
9841065 ) ;
9851066 }
1067+
9861068 transactionFrom = transfer . from ;
9871069 transactionTo = transfer . to ;
9881070 const amount = transfer . amount ;
9891071
1072+ logger . info ( 'Successfully extracted ERC-20 transfer from AA transaction' , {
1073+ txHash,
1074+ from : transactionFrom ,
1075+ to : transactionTo ,
1076+ amount,
1077+ } ) ;
1078+
9901079 if ( ! receipt . status ) {
9911080 throw new Error (
9921081 i18n . __ (
0 commit comments