@@ -14,14 +14,21 @@ import { tryGetBlockExplorerAddressUrl } from '../../../utils/url';
1414import { MessageDebugResult } from '../../debugger/types' ;
1515import {
1616 decodeIcaBody ,
17+ decodeIcaCallData ,
18+ decodeMulticallIcaCalls ,
1719 IcaMessageType ,
1820 useCcipReadIsmUrls ,
1921 useIcaAddress ,
2022 useRelatedIcaMessage ,
2123 useRevealCalls ,
2224} from '../ica' ;
25+ import { CollapsibleLabelAndCodeBlock } from './CodeBlock' ;
2326import { KeyValueRow } from './KeyValueRow' ;
2427
28+ interface DisplayIcaCall extends IcaCall {
29+ nestedCalls ?: IcaCall [ ] ;
30+ }
31+
2532/**
2633 * Check if a bytes32 salt contains an address (first 12 bytes are zeros, last 20 bytes are non-zero).
2734 * This is surfaced as User only for ICA user-salt mode, where the router encodes the EOA in bytes12(0) + address.
@@ -138,6 +145,10 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
138145 const originChainName = chainMetadataResolver . tryGetChainName ( originDomainId ) || undefined ;
139146 const destinationChainName =
140147 chainMetadataResolver . tryGetChainName ( destinationDomainId ) || undefined ;
148+ const tryGetChainName = useCallback (
149+ ( domainId : number ) => chainMetadataResolver . tryGetChainName ( domainId ) || undefined ,
150+ [ chainMetadataResolver ] ,
151+ ) ;
141152 const destinationNativeDecimals =
142153 chainMetadataResolver . tryGetChainMetadata ( destinationDomainId ) ?. nativeToken ?. decimals ?? 18 ;
143154
@@ -207,14 +218,20 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
207218
208219 // Combine calls from message body (CALLS type) or from reveal metadata (REVEAL type)
209220 const displayCalls = useMemo ( ( ) => {
221+ const attachNestedCalls = ( calls : IcaCall [ ] ) : DisplayIcaCall [ ] =>
222+ calls . map ( ( call ) => ( {
223+ ...call ,
224+ nestedCalls : decodeMulticallIcaCalls ( call , destinationChainName ) ?? undefined ,
225+ } ) ) ;
226+
210227 if ( decodeResult ?. messageType === IcaMessageType . CALLS ) {
211- return decodeResult . calls ;
228+ return attachNestedCalls ( decodeResult . calls ) ;
212229 }
213230 if ( decodeResult ?. messageType === IcaMessageType . REVEAL && revealCalls ) {
214- return revealCalls ;
231+ return attachNestedCalls ( revealCalls ) ;
215232 }
216233 return [ ] ;
217- } , [ decodeResult , revealCalls ] ) ;
234+ } , [ decodeResult , revealCalls , destinationChainName ] ) ;
218235
219236 // Get the failed call index from debug result (-1 if no failure or not available)
220237 const failedCallIndex = debugResult ?. icaDetails ?. failedCallIndex ?? - 1 ;
@@ -237,6 +254,19 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
237254 ) ,
238255 ] as const ,
239256 ) ,
257+ ...displayCalls . flatMap ( ( call , i ) =>
258+ ( call . nestedCalls ?? [ ] ) . map (
259+ async ( nestedCall , nestedIndex ) =>
260+ [
261+ `call-${ i } -${ nestedIndex } ` ,
262+ await tryGetBlockExplorerAddressUrl (
263+ chainMetadataResolver ,
264+ destinationDomainId ,
265+ nestedCall . to ,
266+ ) ,
267+ ] as const ,
268+ ) ,
269+ ) ,
240270 displayOwner
241271 ? ( [
242272 'owner' ,
@@ -617,8 +647,10 @@ export function IcaDetailsCard({ message, blur, debugResult }: Props) {
617647 index = { i }
618648 total = { displayCalls . length }
619649 explorerUrl = { explorerUrls [ `call-${ i } ` ] }
650+ nestedExplorerUrls = { explorerUrls }
620651 blur = { blur }
621652 failedCallIndex = { failedCallIndex }
653+ tryGetChainName = { tryGetChainName }
622654 nativeDecimals = { destinationNativeDecimals }
623655 />
624656 ) ) }
@@ -649,18 +681,26 @@ function IcaCallDetails({
649681 index,
650682 total,
651683 explorerUrl,
684+ nestedExplorerUrls,
652685 blur,
653686 failedCallIndex,
687+ tryGetChainName,
654688 nativeDecimals,
655689} : {
656- call : IcaCall ;
690+ call : DisplayIcaCall ;
657691 index : number ;
658692 total : number ;
659693 explorerUrl : string | null | undefined ;
694+ nestedExplorerUrls : Record < string , string | null > ;
660695 blur : boolean ;
661696 failedCallIndex : number ;
697+ tryGetChainName : ( domainId : number ) => string | undefined ;
662698 nativeDecimals : number ;
663699} ) {
700+ const decodedCallData = useMemo (
701+ ( ) => decodeIcaCallData ( call . data , tryGetChainName ) ,
702+ [ call . data , tryGetChainName ] ,
703+ ) ;
664704 const { hasValue, formattedValue } = useMemo (
665705 ( ) => getFormattedCallValue ( call . value , nativeDecimals ) ,
666706 [ call . value , nativeDecimals ] ,
@@ -671,14 +711,17 @@ function IcaCallDetails({
671711 return (
672712 < div
673713 className = { clsx (
674- 'rounded border p-3' ,
675- isFailed ? 'border-red-200 bg-red-50' : 'border-gray-200 bg-gray -50',
714+ 'rounded border border-gray-200 bg-gray-50 p-3' ,
715+ isFailed && 'border-red-200 bg-red-50' ,
676716 ) }
677717 >
678- < label className = { clsx ( 'text-xs font-medium' , isFailed ? 'text-red-600' : 'text-gray-600' ) } >
679- { `Call ${ index + 1 } of ${ total } ${ isFailed ? ' - Failed' : '' } ` }
680- </ label >
718+ < div className = "flex flex-wrap items-baseline gap-x-2 gap-y-1" >
719+ < label className = { clsx ( 'text-xs font-medium' , isFailed ? 'text-red-600' : 'text-gray-700' ) } >
720+ { `Call ${ index + 1 } of ${ total } ${ isFailed ? ' - Failed' : '' } ` }
721+ </ label >
722+ </ div >
681723 < div className = "mt-2 space-y-2" >
724+ { decodedCallData && < DecodedCallBanner decodedCallData = { decodedCallData } blur = { blur } /> }
682725 < KeyValueRow
683726 label = "Target:"
684727 labelWidth = "w-16 sm:w-20"
@@ -698,15 +741,139 @@ function IcaCallDetails({
698741 blurValue = { blur }
699742 />
700743 ) }
744+ { decodedCallData ? (
745+ < CollapsibleLabelAndCodeBlock label = "Raw data:" value = { call . data } />
746+ ) : (
747+ < KeyValueRow
748+ label = "Data:"
749+ labelWidth = "w-16 sm:w-20"
750+ display = { call . data }
751+ displayWidth = "w-52 sm:w-80 lg:w-96"
752+ showCopy = { true }
753+ blurValue = { blur }
754+ />
755+ ) }
756+ { ! ! call . nestedCalls ?. length && (
757+ < div className = "mt-3 space-y-2 border-l-2 border-gray-200 pl-3" >
758+ < div className = "text-xs font-medium text-gray-600" > Decoded multicall:</ div >
759+ { call . nestedCalls . map ( ( nestedCall , nestedIndex ) => (
760+ < NestedIcaCallDetails
761+ key = { `nested-ica-call-${ index } -${ nestedIndex } ` }
762+ call = { nestedCall }
763+ index = { nestedIndex }
764+ total = { call . nestedCalls ?. length ?? 0 }
765+ explorerUrl = { nestedExplorerUrls [ `call-${ index } -${ nestedIndex } ` ] }
766+ blur = { blur }
767+ tryGetChainName = { tryGetChainName }
768+ nativeDecimals = { nativeDecimals }
769+ />
770+ ) ) }
771+ </ div >
772+ ) }
773+ </ div >
774+ </ div >
775+ ) ;
776+ }
777+
778+ function NestedIcaCallDetails ( {
779+ call,
780+ index,
781+ total,
782+ explorerUrl,
783+ blur,
784+ tryGetChainName,
785+ nativeDecimals,
786+ } : {
787+ call : IcaCall ;
788+ index : number ;
789+ total : number ;
790+ explorerUrl : string | null | undefined ;
791+ blur : boolean ;
792+ tryGetChainName : ( domainId : number ) => string | undefined ;
793+ nativeDecimals : number ;
794+ } ) {
795+ const decodedCallData = useMemo (
796+ ( ) => decodeIcaCallData ( call . data , tryGetChainName ) ,
797+ [ call . data , tryGetChainName ] ,
798+ ) ;
799+ const { hasValue, formattedValue } = useMemo (
800+ ( ) => getFormattedCallValue ( call . value , nativeDecimals ) ,
801+ [ call . value , nativeDecimals ] ,
802+ ) ;
803+
804+ return (
805+ < div className = "rounded border border-gray-200 bg-white p-3" >
806+ < div className = "flex flex-wrap items-baseline gap-x-2 gap-y-1" >
807+ < label className = "text-xs font-medium text-gray-600" >
808+ { `Nested call ${ index + 1 } of ${ total } ` }
809+ </ label >
810+ </ div >
811+ < div className = "mt-2 space-y-2" >
812+ { decodedCallData && < DecodedCallBanner decodedCallData = { decodedCallData } blur = { blur } /> }
701813 < KeyValueRow
702- label = "Data :"
814+ label = "Target :"
703815 labelWidth = "w-16 sm:w-20"
704- display = { call . data }
705- displayWidth = "w-52 sm:w-80 lg:w-96"
816+ display = { call . to }
817+ displayWidth = "w-52 sm:w-72"
818+ link = { explorerUrl || undefined }
706819 showCopy = { true }
707820 blurValue = { blur }
708821 />
822+ { hasValue && (
823+ < KeyValueRow
824+ label = "Value:"
825+ labelWidth = "w-16 sm:w-20"
826+ display = { `${ formattedValue } (native)` }
827+ displayWidth = "w-52 sm:w-72"
828+ showCopy = { false }
829+ blurValue = { blur }
830+ />
831+ ) }
832+ { decodedCallData ? (
833+ < CollapsibleLabelAndCodeBlock label = "Raw data:" value = { call . data } />
834+ ) : (
835+ < KeyValueRow
836+ label = "Data:"
837+ labelWidth = "w-16 sm:w-20"
838+ display = { call . data }
839+ displayWidth = "w-52 sm:w-80 lg:w-96"
840+ showCopy = { true }
841+ blurValue = { blur }
842+ />
843+ ) }
709844 </ div >
710845 </ div >
711846 ) ;
712847}
848+
849+ function DecodedCallBanner ( {
850+ decodedCallData,
851+ blur,
852+ } : {
853+ decodedCallData : NonNullable < ReturnType < typeof decodeIcaCallData > > ;
854+ blur : boolean ;
855+ } ) {
856+ return (
857+ < div className = "rounded bg-primary-50 px-3 py-2 text-xs text-primary-700" >
858+ < div >
859+ < span className = "font-medium" > { decodedCallData . functionName } ():</ span > { ' ' }
860+ { decodedCallData . summary }
861+ </ div >
862+ { ! ! decodedCallData . details ?. length && (
863+ < div className = "mt-1.5 grid grid-cols-1 gap-y-1 sm:grid-cols-2 sm:gap-x-3" >
864+ { decodedCallData . details . map ( ( detail ) => (
865+ < KeyValueRow
866+ key = { `${ detail . label } -${ detail . value } ` }
867+ label = { detail . label }
868+ labelWidth = "w-24 sm:w-28"
869+ display = { detail . value }
870+ displayWidth = "w-52 sm:w-72"
871+ showCopy = { detail . value . startsWith ( '0x' ) }
872+ blurValue = { blur }
873+ />
874+ ) ) }
875+ </ div >
876+ ) }
877+ </ div >
878+ ) ;
879+ }
0 commit comments